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 #193 from EvanTechDev/feature/500

fix(backup): persist restored settings and harden blob api errors

authored by

Evan Huang and committed by
GitHub
7b36dfb0 4bc8c7bf

+95 -64
+87 -63
app/api/blob/route.ts
··· 6 6 7 7 export const runtime = "nodejs"; 8 8 9 + const postgresUrl = process.env.POSTGRES_URL; 10 + const useSsl = 11 + postgresUrl && 12 + !/localhost|127\.0\.0\.1/.test(postgresUrl) && 13 + !/sslmode=disable/.test(postgresUrl); 14 + 9 15 const pool = new Pool({ 10 - connectionString: process.env.POSTGRES_URL, 11 - ssl: { rejectUnauthorized: false } 16 + connectionString: postgresUrl, 17 + ssl: useSsl ? { rejectUnauthorized: false } : undefined, 12 18 }); 13 19 14 20 let inited = false; 15 21 16 22 async function initDB() { 23 + if (!postgresUrl) { 24 + throw new Error("POSTGRES_URL is not configured"); 25 + } 17 26 if (inited) return; 18 27 const client = await pool.connect(); 19 28 try { ··· 94 103 } 95 104 96 105 export async function GET() { 97 - const atproto = await getAtprotoSession(); 98 - if (atproto) { 106 + try { 107 + const atproto = await getAtprotoSession(); 108 + if (atproto) { 109 + try { 110 + const record = await getRecord({ 111 + pds: atproto.pds, 112 + repo: atproto.did, 113 + collection: ATPROTO_BACKUP_COLLECTION, 114 + rkey: ATPROTO_BACKUP_RKEY, 115 + accessToken: atproto.accessToken, 116 + dpopPrivateKeyPem: atproto.dpopPrivateKeyPem, 117 + dpopPublicJwk: atproto.dpopPublicJwk, 118 + }); 119 + const value = record.value ?? {}; 120 + return NextResponse.json({ 121 + ciphertext: value.ciphertext, 122 + iv: value.iv, 123 + timestamp: value.updatedAt, 124 + backend: "atproto", 125 + }); 126 + } catch { 127 + return NextResponse.json({ error: "Not found" }, { status: 404 }); 128 + } 129 + } 130 + 131 + const user = await currentUser(); 132 + if (!user) 133 + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 134 + 135 + await initDB(); 136 + 137 + const client = await pool.connect(); 99 138 try { 100 - const record = await getRecord({ 139 + const result = await client.query( 140 + `SELECT encrypted_data, iv, timestamp FROM calendar_backups WHERE user_id = $1`, 141 + [user.id], 142 + ); 143 + if (result.rowCount === 0) 144 + return NextResponse.json({ error: "Not found" }, { status: 404 }); 145 + 146 + return NextResponse.json({ 147 + ciphertext: result.rows[0].encrypted_data, 148 + iv: result.rows[0].iv, 149 + timestamp: result.rows[0].timestamp, 150 + backend: "postgres", 151 + }); 152 + } finally { 153 + client.release(); 154 + } 155 + } catch (e: unknown) { 156 + const message = e instanceof Error ? e.message : "Internal error"; 157 + return NextResponse.json({ error: message }, { status: 500 }); 158 + } 159 + } 160 + 161 + export async function DELETE() { 162 + try { 163 + const atproto = await getAtprotoSession(); 164 + if (atproto) { 165 + await deleteRecord({ 101 166 pds: atproto.pds, 102 167 repo: atproto.did, 103 168 collection: ATPROTO_BACKUP_COLLECTION, ··· 106 171 dpopPrivateKeyPem: atproto.dpopPrivateKeyPem, 107 172 dpopPublicJwk: atproto.dpopPublicJwk, 108 173 }); 109 - const value = record.value ?? {}; 110 - return NextResponse.json({ 111 - ciphertext: value.ciphertext, 112 - iv: value.iv, 113 - timestamp: value.updatedAt, 114 - backend: "atproto", 115 - }); 116 - } catch { 117 - return NextResponse.json({ error: "Not found" }, { status: 404 }); 174 + return NextResponse.json({ success: true, backend: "atproto" }); 118 175 } 119 - } 120 176 121 - const user = await currentUser(); 122 - if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 177 + const user = await currentUser(); 178 + if (!user) 179 + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 123 180 124 - await initDB(); 125 - 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 }); 181 + await initDB(); 133 182 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(); 183 + const client = await pool.connect(); 184 + try { 185 + await client.query(`DELETE FROM calendar_backups WHERE user_id = $1`, [ 186 + user.id, 187 + ]); 188 + return NextResponse.json({ success: true, backend: "postgres" }); 189 + } finally { 190 + client.release(); 191 + } 192 + } catch (e: unknown) { 193 + const message = e instanceof Error ? e.message : "Internal error"; 194 + return NextResponse.json({ error: message }, { status: 500 }); 142 195 } 143 196 } 144 - 145 - export async function DELETE() { 146 - const atproto = await getAtprotoSession(); 147 - 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 - }); 157 - return NextResponse.json({ success: true, backend: "atproto" }); 158 - } 159 - 160 - const user = await currentUser(); 161 - if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 162 - 163 - await initDB(); 164 - 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(); 171 - } 172 - }
+8 -1
components/app/profile/user-profile-button.tsx
··· 57 57 import { 58 58 readInMemoryStorage, 59 59 clearEncryptionPassword, 60 + isSensitiveStorageKey, 60 61 markEncryptedSnapshot, 61 62 readEncryptedLocalStorage, 62 63 setEncryptionPassword, ··· 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 + ); 137 144 }), 138 145 ); 139 146 } ··· 1183 1190 </Dialog> 1184 1191 </> 1185 1192 ); 1186 - } 1193 + }