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
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

restore

+55 -137
+46 -111
app/api/blob/route.ts
··· 6 6 7 7 export const runtime = "nodejs"; 8 8 9 - let pool: Pool | null = null; 10 - let inited = false; 11 - 12 - const ATPROTO_BACKUP_COLLECTION = "app.onecalendar.backup"; 13 - const ATPROTO_BACKUP_RKEY = "latest"; 14 - 15 - function getPool() { 16 - if (pool) return pool; 17 - 18 - const connectionString = process.env.POSTGRES_URL; 19 - if (!connectionString) { 20 - return null; 21 - } 22 - 23 - pool = new Pool({ 24 - connectionString, 25 - ssl: { rejectUnauthorized: false }, 26 - }); 9 + const pool = new Pool({ 10 + connectionString: process.env.POSTGRES_URL, 11 + ssl: { rejectUnauthorized: false } 12 + }); 27 13 28 - return pool; 29 - } 14 + let inited = false; 30 15 31 16 async function initDB() { 32 - if (inited) return true; 33 - const currentPool = getPool(); 34 - if (!currentPool) return false; 35 - 36 - const client = await currentPool.connect(); 17 + if (inited) return; 18 + const client = await pool.connect(); 37 19 try { 38 20 await client.query(` 39 21 CREATE TABLE IF NOT EXISTS calendar_backups ( ··· 44 26 ) 45 27 `); 46 28 inited = true; 47 - return true; 48 29 } finally { 49 30 client.release(); 50 31 } 51 32 } 33 + 34 + const ATPROTO_BACKUP_COLLECTION = "app.onecalendar.backup"; 35 + const ATPROTO_BACKUP_RKEY = "latest"; 52 36 53 37 export async function POST(req: NextRequest) { 54 38 try { ··· 81 65 } 82 66 83 67 const user = await currentUser(); 84 - if (!user) 85 - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 68 + if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 86 69 87 - const dbReady = await initDB(); 88 - if (!dbReady) { 89 - return NextResponse.json( 90 - { error: "Backup storage is not configured" }, 91 - { status: 503 }, 92 - ); 93 - } 70 + await initDB(); 94 71 95 - const currentPool = getPool(); 96 - if (!currentPool) { 97 - return NextResponse.json( 98 - { error: "Backup storage is not configured" }, 99 - { status: 503 }, 100 - ); 101 - } 102 - 103 - const client = await currentPool.connect(); 72 + const client = await pool.connect(); 104 73 try { 105 74 await client.query( 106 75 ` ··· 152 121 const user = await currentUser(); 153 122 if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 154 123 124 + await initDB(); 125 + 126 + const client = await pool.connect(); 155 127 try { 156 - const dbReady = await initDB(); 157 - if (!dbReady) { 158 - return NextResponse.json({ error: "Not found" }, { status: 404 }); 159 - } 128 + const result = await client.query( 129 + `SELECT encrypted_data, iv, timestamp FROM calendar_backups WHERE user_id = $1`, 130 + [user.id], 131 + ); 132 + if (result.rowCount === 0) return NextResponse.json({ error: "Not found" }, { status: 404 }); 160 133 161 - const currentPool = getPool(); 162 - if (!currentPool) { 163 - return NextResponse.json({ error: "Not found" }, { status: 404 }); 164 - } 165 - 166 - const client = await currentPool.connect(); 167 - try { 168 - const result = await client.query( 169 - `SELECT encrypted_data, iv, timestamp FROM calendar_backups WHERE user_id = $1`, 170 - [user.id], 171 - ); 172 - if (result.rowCount === 0) 173 - return NextResponse.json({ error: "Not found" }, { status: 404 }); 174 - 175 - return NextResponse.json({ 176 - ciphertext: result.rows[0].encrypted_data, 177 - iv: result.rows[0].iv, 178 - timestamp: result.rows[0].timestamp, 179 - backend: "postgres", 180 - }); 181 - } finally { 182 - client.release(); 183 - } 184 - } catch (e: unknown) { 185 - const message = e instanceof Error ? e.message : "Internal error"; 186 - return NextResponse.json({ error: message }, { status: 500 }); 134 + return NextResponse.json({ 135 + ciphertext: result.rows[0].encrypted_data, 136 + iv: result.rows[0].iv, 137 + timestamp: result.rows[0].timestamp, 138 + backend: "postgres", 139 + }); 140 + } finally { 141 + client.release(); 187 142 } 188 143 } 189 144 190 145 export async function DELETE() { 191 146 const atproto = await getAtprotoSession(); 192 147 if (atproto) { 193 - try { 194 - await deleteRecord({ 195 - pds: atproto.pds, 196 - repo: atproto.did, 197 - collection: ATPROTO_BACKUP_COLLECTION, 198 - rkey: ATPROTO_BACKUP_RKEY, 199 - accessToken: atproto.accessToken, 200 - dpopPrivateKeyPem: atproto.dpopPrivateKeyPem, 201 - dpopPublicJwk: atproto.dpopPublicJwk, 202 - }); 203 - } catch { 204 - return NextResponse.json({ success: true, backend: "atproto" }); 205 - } 206 - 148 + await deleteRecord({ 149 + pds: atproto.pds, 150 + repo: atproto.did, 151 + collection: ATPROTO_BACKUP_COLLECTION, 152 + rkey: ATPROTO_BACKUP_RKEY, 153 + accessToken: atproto.accessToken, 154 + dpopPrivateKeyPem: atproto.dpopPrivateKeyPem, 155 + dpopPublicJwk: atproto.dpopPublicJwk, 156 + }); 207 157 return NextResponse.json({ success: true, backend: "atproto" }); 208 158 } 209 159 210 160 const user = await currentUser(); 211 161 if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 212 162 213 - try { 214 - const dbReady = await initDB(); 215 - if (!dbReady) { 216 - return NextResponse.json({ success: true, backend: "postgres" }); 217 - } 163 + await initDB(); 218 164 219 - const currentPool = getPool(); 220 - if (!currentPool) { 221 - return NextResponse.json({ success: true, backend: "postgres" }); 222 - } 223 - 224 - const client = await currentPool.connect(); 225 - try { 226 - await client.query(`DELETE FROM calendar_backups WHERE user_id = $1`, [ 227 - user.id, 228 - ]); 229 - return NextResponse.json({ success: true, backend: "postgres" }); 230 - } finally { 231 - client.release(); 232 - } 233 - } catch (e: unknown) { 234 - const message = e instanceof Error ? e.message : "Internal error"; 235 - return NextResponse.json({ error: message }, { status: 500 }); 165 + const client = await pool.connect(); 166 + try { 167 + await client.query(`DELETE FROM calendar_backups WHERE user_id = $1`, [user.id]); 168 + return NextResponse.json({ success: true, backend: "postgres" }); 169 + } finally { 170 + client.release(); 236 171 } 237 - } 172 + }
+5 -1
components/app/calendar.tsx
··· 189 189 const [toastPosition, setToastPosition] = useLocalStorage< 190 190 "bottom-left" | "bottom-center" | "bottom-right" 191 191 >("toast-position", "bottom-right"); 192 + const hasAppliedDefaultView = useRef(false); 193 + 192 194 useEffect(() => { 195 + if (hasAppliedDefaultView.current) return; 193 196 setView(isCalendarView(defaultView) ? defaultView : "week"); 197 + hasAppliedDefaultView.current = true; 194 198 }, [defaultView]); 195 199 196 200 useEffect(() => { ··· 1087 1091 </div> 1088 1092 </div> 1089 1093 ); 1090 - } 1094 + }
+1 -22
components/app/profile/user-profile-button.tsx
··· 61 61 readEncryptedLocalStorage, 62 62 setEncryptionPassword, 63 63 writeInMemoryStorage, 64 - isSensitiveStorageKey, 65 64 } from "@/hooks/useLocalStorage"; 66 65 67 66 const AUTO_KEY = "auto-backup-enabled"; ··· 135 134 } 136 135 writeInMemoryStorage(key, normalized); 137 136 markEncryptedSnapshot(key, normalized); 138 - if (!isSensitiveStorageKey(key)) { 139 - localStorage.setItem(key, normalized); 140 - } 141 - window.dispatchEvent( 142 - new CustomEvent("local-storage-written", { detail: { key } }), 143 - ); 144 - if (key === "preferred-language") { 145 - try { 146 - const language = JSON.parse(normalized); 147 - if (typeof language === "string") { 148 - window.dispatchEvent( 149 - new CustomEvent("languagechange", { 150 - detail: { language }, 151 - }), 152 - ); 153 - } 154 - } catch { 155 - // Ignore invalid language payloads 156 - } 157 - } 158 137 }), 159 138 ); 160 139 } ··· 1204 1183 </Dialog> 1205 1184 </> 1206 1185 ); 1207 - } 1186 + }
+3 -3
hooks/useLocalStorage.ts
··· 221 221 if (typeof window === "undefined") return 222 222 try { 223 223 const raw = JSON.stringify(value) 224 - inMemoryStorage.set(key, raw) 225 224 if (ENCRYPTION_STATE.enabled && ENCRYPTION_STATE.password) { 226 225 if (isSensitiveStorageKey(key)) { 226 + inMemoryStorage.set(key, raw) 227 227 encryptedSnapshots.set(key, { value: raw, failed: false }) 228 228 window.localStorage.removeItem(key) 229 229 emitStorageWrite(key) ··· 290 290 if (typeof window === "undefined") return 291 291 try { 292 292 const raw = JSON.stringify(value) 293 - inMemoryStorage.set(key, raw) 294 293 if (ENCRYPTION_STATE.enabled && ENCRYPTION_STATE.password) { 295 294 if (isSensitiveStorageKey(key)) { 295 + inMemoryStorage.set(key, raw) 296 296 encryptedSnapshots.set(key, { value: raw, failed: false }) 297 297 window.localStorage.removeItem(key) 298 298 emitStorageWrite(key) ··· 381 381 }, [key, serializedValue, storedValue]) 382 382 383 383 return [storedValue, setStoredValue] as const 384 - } 384 + }