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.

Merge pull request #192 from EvanTechDev/feature/refactor-settings-handling-and-fix-backup-bug

fix(settings): persist restored preferences and harden blob backup API

authored by

Evan Huang and committed by
GitHub
0d9faa8f cb68a68d

+131 -49
+110 -45
app/api/blob/route.ts
··· 6 6 7 7 export const runtime = "nodejs"; 8 8 9 - const pool = new Pool({ 10 - connectionString: process.env.POSTGRES_URL, 11 - ssl: { rejectUnauthorized: false } 12 - }); 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 + }); 13 27 14 - let inited = false; 28 + return pool; 29 + } 15 30 16 31 async function initDB() { 17 - if (inited) return; 18 - const client = await pool.connect(); 32 + if (inited) return true; 33 + const currentPool = getPool(); 34 + if (!currentPool) return false; 35 + 36 + const client = await currentPool.connect(); 19 37 try { 20 38 await client.query(` 21 39 CREATE TABLE IF NOT EXISTS calendar_backups ( ··· 26 44 ) 27 45 `); 28 46 inited = true; 47 + return true; 29 48 } finally { 30 49 client.release(); 31 50 } 32 51 } 33 - 34 - const ATPROTO_BACKUP_COLLECTION = "app.onecalendar.backup"; 35 - const ATPROTO_BACKUP_RKEY = "latest"; 36 52 37 53 export async function POST(req: NextRequest) { 38 54 try { ··· 65 81 } 66 82 67 83 const user = await currentUser(); 68 - if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 84 + if (!user) 85 + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 69 86 70 - await initDB(); 87 + const dbReady = await initDB(); 88 + if (!dbReady) { 89 + return NextResponse.json( 90 + { error: "Backup storage is not configured" }, 91 + { status: 503 }, 92 + ); 93 + } 71 94 72 - const client = await pool.connect(); 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(); 73 104 try { 74 105 await client.query( 75 106 ` ··· 121 152 const user = await currentUser(); 122 153 if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 123 154 124 - await initDB(); 155 + try { 156 + const dbReady = await initDB(); 157 + if (!dbReady) { 158 + return NextResponse.json({ error: "Not found" }, { status: 404 }); 159 + } 160 + 161 + const currentPool = getPool(); 162 + if (!currentPool) { 163 + return NextResponse.json({ error: "Not found" }, { status: 404 }); 164 + } 125 165 126 - const client = await pool.connect(); 127 - try { 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 }); 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 }); 133 174 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(); 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 }); 142 187 } 143 188 } 144 189 145 190 export async function DELETE() { 146 191 const atproto = await getAtprotoSession(); 147 192 if (atproto) { 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 - }); 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 + 157 207 return NextResponse.json({ success: true, backend: "atproto" }); 158 208 } 159 209 160 210 const user = await currentUser(); 161 211 if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 162 212 163 - await initDB(); 164 - 165 - const client = await pool.connect(); 166 213 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(); 214 + const dbReady = await initDB(); 215 + if (!dbReady) { 216 + return NextResponse.json({ success: true, backend: "postgres" }); 217 + } 218 + 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 }); 171 236 } 172 237 }
-4
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 - 194 192 useEffect(() => { 195 - if (hasAppliedDefaultView.current) return; 196 193 setView(isCalendarView(defaultView) ? defaultView : "week"); 197 - hasAppliedDefaultView.current = true; 198 194 }, [defaultView]); 199 195 200 196 useEffect(() => {
+21
components/app/profile/user-profile-button.tsx
··· 61 61 readEncryptedLocalStorage, 62 62 setEncryptionPassword, 63 63 writeInMemoryStorage, 64 + isSensitiveStorageKey, 64 65 } from "@/hooks/useLocalStorage"; 65 66 66 67 const AUTO_KEY = "auto-backup-enabled"; ··· 134 135 } 135 136 writeInMemoryStorage(key, normalized); 136 137 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 + } 137 158 }), 138 159 ); 139 160 }