slowly getting to something

Changed files
+129 -7
atproto-notifications
server
+69
atproto-notifications/src/components/SecretPassword.jsx
··· 1 + import { useCallback, useState } from 'react'; 2 + import { PostJson } from './Fetch'; 3 + 4 + export function SecretPassword({ did, role }) { 5 + const [begun, setBegun] = useState(false); 6 + const [pw, setPw] = useState(''); 7 + const [submitting, setSubmitting] = useState(false); 8 + 9 + const handleSubmit = useCallback(e => { 10 + e.preventDefault(); 11 + setSubmitting(true); 12 + }) 13 + 14 + return ( 15 + <form method="post" onSubmit={handleSubmit}> 16 + <h2>Secret password required</h2> 17 + <p>This demo is not ready for public yet, but you can get early access as a <a href="https://github.com/sponsors/uniphil/" target="_blank">github sponsor</a> or <a href="https://ko-fi.com/bad_example" target="_blank">ko-fi supporter</a>.</p> 18 + 19 + {submitting ? ( 20 + <PostJson 21 + endpoint="/super-top-secret-access" 22 + data={{ secret_password: pw }} 23 + credentials 24 + loading={() => (<>Checking&hellip;</>)} 25 + error={e => { 26 + console.log('err', e); 27 + return ( 28 + <> 29 + <p>whateverrrr</p> 30 + <p> 31 + <button onClick={() => setSubmitting(false)}>retry</button> 32 + </p> 33 + </> 34 + ); 35 + }} 36 + ok={() => ( 37 + <> 38 + <p>That will do.</p> 39 + <p> 40 + <button onClick={() => window.location.reload()}> 41 + Enter 42 + </button> 43 + </p> 44 + </> 45 + )} 46 + /> 47 + ) : ( 48 + <p> 49 + <label> 50 + Password: 51 + {' '} 52 + <input 53 + type="text" 54 + value={pw} 55 + onFocus={() => setBegun(true)} 56 + onChange={e => setPw(e.target.value)} 57 + /> 58 + </label> 59 + {' '} 60 + {begun && ( 61 + <button type="submit" className="subtle"> 62 + open sesame 63 + </button> 64 + )} 65 + </p> 66 + )} 67 + </form> 68 + ); 69 + }
+11 -3
atproto-notifications/src/components/setup/WithServerHello.tsx
··· 1 1 import { useCallback, useEffect, useState } from 'react'; 2 - import { PushServerContext } from '../../context'; 2 + import { UserContext, PushServerContext } from '../../context'; 3 3 import { WhoAmI } from '../WhoAmI'; 4 + import { SecretPassword } from '../SecretPassword'; 4 5 import { GetJson, PostJson } from '../Fetch'; 5 6 import { Chrome } from './Chrome'; 6 7 ··· 18 19 export function WithServerHello({ children }) { 19 20 const [whoamiInfo, setWhoamiInfo] = useState(null); 20 21 22 + const childrenFor = useCallback((did, role) => { 23 + if (role === 'public') { 24 + return <SecretPassword did={did} role={role} />; 25 + } 26 + return 'hiiiiiiii ' + role; 27 + }) 28 + 21 29 return ( 22 30 <GetJson 23 31 /* todo: key on login state */ ··· 38 46 ok={({ did, role, webPushPublicKey }) => ( 39 47 <Chrome user={{ did, role }}> 40 48 <PushServerContext.Provider value={webPushPublicKey}> 41 - {children} 49 + {childrenFor(did, role)} 42 50 </PushServerContext.Provider> 43 51 </Chrome> 44 52 )} ··· 48 56 return ( 49 57 <Chrome user={{ did, role }}> 50 58 <PushServerContext.Provider value={webPushPublicKey}> 51 - {children} 59 + {childrenFor(did, role)} 52 60 </PushServerContext.Provider> 53 61 </Chrome> 54 62 );
+19
server/db.js
··· 14 14 #stmt_update_push_sub; 15 15 #stmt_delete_push_sub; 16 16 #stmt_get_push_info; 17 + #stmt_set_role; 17 18 #transactionally; 18 19 #db; 19 20 ··· 42 43 43 44 this.#stmt_get_account = db.prepare( 44 45 `select a.first_seen, 46 + a.role, 45 47 count(*) as total_subs 46 48 from accounts a 47 49 left outer join push_subs p on (p.account_did = a.did) ··· 87 89 from push_subs 88 90 where account_did = ?`); 89 91 92 + this.#stmt_set_role = db.prepare( 93 + `update accounts 94 + set role = ?, 95 + secret_password = ? 96 + where did = ?`); 97 + 90 98 this.#transactionally = t => db.transaction(t).immediate(); 91 99 } 92 100 ··· 94 102 this.#stmt_insert_account.run(did); 95 103 } 96 104 105 + getAccount(did) { 106 + return this.#stmt_get_account.get(did); 107 + } 108 + 97 109 addPushSub(did, session, sub) { 98 110 this.#transactionally(() => { 99 111 const res = this.#stmt_get_account.get(did); ··· 121 133 122 134 deleteSub(session) { 123 135 this.#stmt_delete_push_sub.run(session); 136 + } 137 + 138 + setRole(did, role, secret_password) { 139 + let res = this.#stmt_set_role.run(role, secret_password, did); 140 + if (res.changes === 0) { 141 + throw new Error('no changes'); 142 + } 124 143 } 125 144 }
+26 -2
server/index.js
··· 198 198 '', 199 199 { ...COOKIE_BASE, expires: new Date(0) }, 200 200 )); 201 + 201 202 const getAccountCookie = (req, res, appSecret, adminDid, noDidCheck = false) => { 202 203 const cookies = cookie.parse(req.headers.cookie ?? ''); 203 204 const untrusted = cookies['verified-account'] ?? ''; ··· 255 256 const handleHello = async (db, req, res, secrets, whoamiHost, adminDid) => { 256 257 const resBase = { webPushPublicKey: secrets.pushKeys.publicKey, whoamiHost }; 257 258 res.setHeader('Content-Type', 'application/json'); 258 - let info = getAccountCookie(req, res, secrets.appSecret, adminDid); 259 + let info = getAccountCookie(req, res, secrets.appSecret, adminDid, true); 259 260 if (info) { 260 261 const [did, _session, isAdmin] = info; 261 - const role = isAdmin ? 'admin' : 'public'; 262 + let { role } = db.getAccount(did); 263 + role = isAdmin ? 'admin' : (role ?? 'public'); 262 264 res 263 265 .setHeader('Content-Type', 'application/json') 264 266 .writeHead(200) ··· 335 337 res.setHeader('Content-Type', 'application/json'); 336 338 res.writeHead(201); 337 339 res.end(JSON.stringify({ sup: 'bye' })); 340 + } 338 341 342 + const handleOpenSesame = async (db, req, res, appSecret) => { 343 + let info = getAccountCookie(req, res, appSecret, null, true); 344 + if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' })); 345 + const [did, _session, _isAdmin] = info; 346 + const body = await getRequesBody(req); 347 + const { secret_password } = JSON.parse(body); 348 + console.log({ secret_password }); 349 + const role = 'early'; 350 + db.setRole(did, role, secret_password); 351 + res.setHeader('Content-Type', 'application/json') 352 + .writeHead(200) 353 + .end('"heyyy"'); 339 354 } 340 355 341 356 const attempt = listener => async (req, res) => { ··· 388 403 if (req.method === 'POST' && req.url === '/logout') { 389 404 res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 390 405 return handleLogout(db, req, res, secrets.appSecret); 406 + } 407 + 408 + if (req.method === 'OPTIONS' && req.url === '/super-top-secret-access') { 409 + // TODO: probably restrict the origin 410 + return res.writeHead(204, CORS_PERMISSIVE(req)).end(); 411 + } 412 + if (req.method === 'POST' && req.url === '/super-top-secret-access') { 413 + res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 414 + return handleOpenSesame(db, req, res, secrets.appSecret); 391 415 } 392 416 393 417 res
+4 -2
server/schema.sql
··· 1 1 create table accounts ( 2 - did text primary key, 3 - first_seen text not null default CURRENT_TIMESTAMP, 2 + did text primary key, 3 + first_seen text not null default CURRENT_TIMESTAMP, 4 + role text null, 5 + secret_password text null, 4 6 5 7 check(did like 'did:%') 6 8 ) strict;