[READ-ONLY] One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links 📅
calendar.xyehr.cn
nextjs
2.5 kB
94 lines
1import { type NextRequest, NextResponse } from 'next/server'
2import { withEvlog, useLogger } from '@/lib/evlog'
3import { getServerSession } from '@/lib/auth-server'
4import crypto from 'crypto'
5import { db } from '@/lib/drizzle/client'
6import { shares } from '@/lib/drizzle/schema'
7import { eq, desc } from 'drizzle-orm'
8
9export const runtime = 'nodejs'
10
11const ALGORITHM = 'aes-256-gcm'
12
13function keyV2Unprotected(shareId: string) {
14 return crypto.createHash('sha256').update(shareId, 'utf8').digest()
15}
16
17function decryptWithKey(
18 encryptedData: string,
19 iv: string,
20 authTag: string,
21 key: Buffer,
22) {
23 const decipher = crypto.createDecipheriv(
24 ALGORITHM,
25 key,
26 Buffer.from(iv, 'hex'),
27 )
28 decipher.setAuthTag(Buffer.from(authTag, 'hex'))
29 let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
30 decrypted += decipher.final('utf8')
31 return decrypted
32}
33
34export const GET = withEvlog(async function GET(_req: NextRequest) {
35 const log = useLogger()
36 const session = await getServerSession()
37 const user = session?.user
38 if (!user) {
39 log.audit?.({
40 action: 'share.list',
41 actor: { type: 'system', id: 'anonymous' },
42 target: { type: 'share_collection', id: 'unknown' },
43 outcome: 'denied',
44 reason: 'Authentication required',
45 })
46 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
47 }
48
49 const result = await db
50 .select()
51 .from(shares)
52 .where(eq(shares.userId, user.id))
53 .orderBy(desc(shares.timestamp))
54
55 const shareList = result.map((row) => {
56 let eventId = ''
57 let eventTitle = ''
58 if (!row.isProtected) {
59 try {
60 const decrypted = decryptWithKey(
61 row.encryptedData,
62 row.iv,
63 row.authTag,
64 keyV2Unprotected(row.shareId),
65 )
66 const dataObj = JSON.parse(decrypted)
67 eventId = dataObj.id ?? ''
68 eventTitle = dataObj.title ?? ''
69 } catch {}
70 } else {
71 eventId = '受保护'
72 eventTitle = '受保护'
73 }
74 return {
75 id: row.shareId,
76 eventId,
77 eventTitle,
78 sharedBy: user.id,
79 shareDate: row.timestamp.toISOString(),
80 shareLink: `/share/${row.shareId}`,
81 isProtected: row.isProtected,
82 }
83 })
84
85 log.audit?.({
86 action: 'share.list',
87 actor: { type: 'user', id: user.id, email: user.email },
88 target: { type: 'share_collection', id: user.id },
89 outcome: 'success',
90 reason: 'User listed shares',
91 })
92
93 return NextResponse.json({ shares: shareList })
94})