demos for spacedust

logout

ish

+71 -15
+14 -6
atproto-notifications/src/App.tsx
··· 67 67 credentials: 'include', 68 68 }); 69 69 if (!res.ok) throw res; 70 + return await res.json(); 70 71 } 71 72 72 73 function App() { ··· 83 84 const onIdentify = useCallback(async details => { 84 85 setVerif('verifying'); 85 86 try { 86 - await verifyUser(host, details.token) 87 + const info = await verifyUser(host, details.token); 87 88 setVerif('verified'); 89 + setRole(info.role); 88 90 setUser(details); 89 91 } catch (e) { 90 92 console.error(e); 91 93 setVerif('failed'); 92 94 } 93 - // setTimeout(() => { 94 - // setVerif('verified'); 95 - // setUser(details); 96 - // }, 400); 97 95 }, [host]); 96 + 97 + const logout = useCallback(async () => { 98 + setRole('anonymous'); 99 + setUser(null); 100 + // TODO: clear indexeddb 101 + await fetch(`${host}/logout`, { 102 + method: 'POST', 103 + credentials: 'include', 104 + }); 105 + }); 98 106 99 107 let hasSW = 'serviceWorker' in navigator; 100 108 let hasPush = 'PushManager' in window; ··· 163 171 <p> 164 172 <span className="handle">@{user.handle}</span> 165 173 {/* TODO: clear *all* info on logout */} 166 - <button className="subtle bad" onClick={() => setUser(null)}>&times;</button> 174 + <button className="subtle bad" onClick={logout}>&times;</button> 167 175 </p> 168 176 </div> 169 177 )}
+6 -1
server/db.js
··· 78 78 79 79 this.#stmt_delete_push_sub = db.prepare( 80 80 `delete from push_subs 81 - where session = ?`); 81 + where account_did = ? 82 + and session = ?`); 82 83 83 84 this.#stmt_get_push_info = db.prepare( 84 85 `select created, ··· 105 106 } 106 107 this.#stmt_insert_push_sub.run(did, session, sub); 107 108 }); 109 + } 110 + 111 + removePushSub(did, session) { 112 + return this.#stmt_delete_push_sub.run(did, session); 108 113 } 109 114 110 115 getSubscribedDids() {
+51 -8
server/index.js
··· 169 169 '', 170 170 { ...COOKIE_BASE, expires: new Date(0) }, 171 171 )); 172 - const getAccountCookie = (req, res, appSecret, adminDid) => { 172 + const getAccountCookie = (req, res, appSecret, adminDid, noDidCheck = false) => { 173 173 const cookies = cookie.parse(req.headers.cookie ?? ''); 174 174 const untrusted = cookies['verified-account'] ?? ''; 175 175 const json = cookieSig.unsign(untrusted, appSecret); ··· 187 187 } 188 188 189 189 // not yet public!! 190 - if (!did || did !== adminDid) { 190 + if (!did || (did !== adminDid && !noDidCheck)) { 191 191 clearAccountCookie(res) 192 192 .setHeader('Content-Type', 'application/json') 193 193 .writeHead(403) ··· 241 241 } 242 242 }; 243 243 244 - const handleVerify = async (db, req, res, whoamiHost, jwks, appSecret) => { 244 + const handleVerify = async (db, req, res, whoamiHost, jwks, appSecret, adminDid) => { 245 245 const body = await getRequesBody(req); 246 246 const { token } = JSON.parse(body); 247 247 let did; ··· 252 252 console.warn('jwks verification failed', e); 253 253 return clearAccountCookie(res).writeHead(400).end(JSON.stringify({ reason: 'verification failed' })); 254 254 } 255 + const isAdmin = did && did === adminDid; 255 256 db.addAccount(did); 256 257 const session = uuidv4(); 257 258 setAccountCookie(res, did, session, appSecret); 258 - return res.writeHead(200).end('okayyyy'); 259 + return res 260 + .setHeader('Content-Type', 'application/json') 261 + .writeHead(200) 262 + .end(JSON.stringify({ did, role: isAdmin ? 'admin' : 'public' })); 259 263 }; 260 264 261 265 const handleSubscribe = async (db, req, res, appSecret, adminDid) => { ··· 265 269 const body = await getRequesBody(req); 266 270 const { sub } = JSON.parse(body); 267 271 // addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG) 268 - db.addPushSub(did, session, JSON.stringify(sub)); 272 + try { 273 + db.addPushSub(did, session, JSON.stringify(sub)); 274 + } catch (e) { 275 + console.warn('failed to add sub', e); 276 + return res 277 + .setHeader('Content-Type', 'application/json') 278 + .writeHead(500) 279 + .end(JSON.stringify({ reason: 'failed to register subscription' })); 280 + } 269 281 updateSubs(db); 270 282 res.setHeader('Content-Type', 'application/json'); 271 283 res.writeHead(201); 272 284 res.end(JSON.stringify({ sup: 'hi' })); 273 285 }; 274 286 287 + const handleLogout = async (db, req, res, appSecret) => { 288 + let info = getAccountCookie(req, res, appSecret, null, true); 289 + if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' })); 290 + const [did, session, _isAdmin] = info; 291 + try { 292 + db.removePushSub(did, session); 293 + } catch (e) { 294 + console.warn('failed to remove sub', e); 295 + return res 296 + .setHeader('Content-Type', 'application/json') 297 + .writeHead(500) 298 + .end(JSON.stringify({ reason: 'failed to register subscription' })); 299 + } 300 + updateSubs(db); 301 + res.setHeader('Content-Type', 'application/json'); 302 + res.writeHead(201); 303 + res.end(JSON.stringify({ sup: 'bye' })); 304 + 305 + } 306 + 275 307 const attempt = listener => async (req, res) => { 276 308 console.log(`-> ${req.method} ${req.url}`); 277 309 try { ··· 303 335 } 304 336 if (req.method === 'POST' && req.url === '/verify') { 305 337 res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 306 - return handleVerify(db, req, res, whoamiHost, jwks, secrets.appSecret); 338 + return handleVerify(db, req, res, whoamiHost, jwks, secrets.appSecret, adminDid); 307 339 } 308 340 309 341 if (req.method === 'OPTIONS' && req.url === '/subscribe') { ··· 315 347 return handleSubscribe(db, req, res, secrets.appSecret, adminDid); 316 348 } 317 349 318 - res.writeHead(200); 319 - res.end('sup'); 350 + if (req.method === 'OPTIONS' && req.url === '/logout') { 351 + // TODO: probably restrict the origin 352 + return res.writeHead(204, CORS_PERMISSIVE(req)).end(); 353 + } 354 + if (req.method === 'POST' && req.url === '/logout') { 355 + res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 356 + return handleLogout(db, req, res, secrets.appSecret); 357 + } 358 + 359 + res 360 + .setHeaders(new Headers(CORS_PERMISSIVE(req))) 361 + .writeHead(404) 362 + .end('not found (sorry)'); 320 363 }); 321 364 322 365 const main = env => {