verifying…
; else { - content =Sorry, failed to verify that identity. please let us know!
{content}>; } @@ -119,7 +131,7 @@ function App() {To show notifications we need permission:
- + > ) } diff --git a/atproto-notifications/src/components/Fetch.tsx b/atproto-notifications/src/components/Fetch.tsx index aa88fba..e4599a5 100644 --- a/atproto-notifications/src/components/Fetch.tsx +++ b/atproto-notifications/src/components/Fetch.tsx @@ -1,5 +1,4 @@ import { useContext, useEffect, useState } from 'react'; -import { HostContext } from '../context' const loadingDefault = () => ( Loading… @@ -52,7 +51,7 @@ async function getJson(url) { } export function GetJson({ endpoint, params, ...forFetch }) { - const host = useContext(HostContext); + const host = import.meta.env.VITE_NOTIFICATIONS_HOST; const url = new URL(endpoint, host); for (let [key, val] of Object.entries(params ?? {})) { url.searchParams.append(key, val); diff --git a/atproto-notifications/src/components/WhoAmI.tsx b/atproto-notifications/src/components/WhoAmI.tsx index 7dccf6e..1b026fb 100644 --- a/atproto-notifications/src/components/WhoAmI.tsx +++ b/atproto-notifications/src/components/WhoAmI.tsx @@ -1,6 +1,6 @@ import { useRef, useEffect } from 'react'; -export function WhoAmI({ onIdentify, origin = 'http://127.0.0.1:9997' }) { +export function WhoAmI({ onIdentify, origin }) { const frameRef = useRef(null); useEffect(() => { diff --git a/atproto-notifications/src/context.ts b/atproto-notifications/src/context.ts deleted file mode 100644 index 5b0a7bb..0000000 --- a/atproto-notifications/src/context.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createContext } from 'react'; - -const HostContext = createContext(null); - -export { HostContext }; diff --git a/gh-pages.sh b/gh-pages.sh index cefcba2..be20d1e 100755 --- a/gh-pages.sh +++ b/gh-pages.sh @@ -6,6 +6,7 @@ git switch gh-pages git merge --no-ff main -m 'merge main' cd atproto-notifications +export VITE_NOTIFICATIONS_HOST=https://notifications-demo-api.microcosm.blue npm run just-build cd .. diff --git a/server/index.js b/server/index.js index 498c144..a523e4c 100755 --- a/server/index.js +++ b/server/index.js @@ -169,7 +169,7 @@ const clearAccountCookie = res => res.setHeader('Set-Cookie', cookie.serialize( '', { ...COOKIE_BASE, expires: new Date(0) }, )); -const getAccountCookie = (req, res, appSecret) => { +const getAccountCookie = (req, res, appSecret, adminDid) => { const cookies = cookie.parse(req.headers.cookie ?? ''); const untrusted = cookies['verified-account'] ?? ''; const json = cookieSig.unsign(untrusted, appSecret); @@ -177,14 +177,26 @@ const getAccountCookie = (req, res, appSecret) => { clearAccountCookie(res); return null; } + let did, session; try { - const [did, session] = JSON.parse(json); - return [did, session]; + [did, session] = JSON.parse(json); } catch (e) { console.warn('validated account cookie but failed to parse json', e); clearAccountCookie(res); return null; } + + // not yet public!! + if (!did || did !== adminDid) { + res.setHeader('Content-Type', 'application/json'); + res.writeHead(403); + clearAccountCookie(res).end(JSON.stringify({ + reason: 'the spacedust notifications demo isn\'t public yet!', + })); + throw new Error('unauthorized'); + } + + return [did, session, did && (did === adminDid)]; }; // never EVER allow user-controllable input into fname (or just fix the path joining) @@ -209,7 +221,27 @@ const handleFile = (fname, ftype) => async (req, res, replace = {}) => { const handleIndex = handleFile('index.html', 'text/html'); const handleServiceWorker = handleFile('service-worker.js', 'application/javascript'); -const handleVerify = async (db, req, res, jwks, appSecret) => { +const handleHello = async (db, req, res, secrets, whoamiHost, adminDid) => { + const resBase = { webPushPublicKey: secrets.pushKeys.publicKey, whoamiHost }; + res.setHeader('Content-Type', 'application/json'); + let info = getAccountCookie(req, res, secrets.appSecret, adminDid); + if (info) { + const [did, _session, isAdmin] = info; + const role = isAdmin ? 'admin' : 'public'; + res + .setHeader('Content-Type', 'application/json') + .writeHead(200) + .end(JSON.stringify({ ...resBase, role, did })); + } else { + res + .setHeader('Content-Type', 'application/json') + .writeHead(200) + .end(JSON.stringify({ ...resBase, role: 'anonymous' })); + } +}; + +const handleVerify = async (db, req, res, whoamiHost, appSecret) => { + const jwks = jose.createRemoteJWKSet(new URL(`${whoamiHost}/.well-known/jwks.json`)); const body = await getRequesBody(req); const { token } = JSON.parse(body); let did; @@ -226,20 +258,9 @@ const handleVerify = async (db, req, res, jwks, appSecret) => { }; const handleSubscribe = async (db, req, res, appSecret, adminDid) => { - let info = getAccountCookie(req, res, appSecret); + let info = getAccountCookie(req, res, appSecret, adminDid); if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' })); - const [did, session] = info; - - // not yet public!! - if (did !== adminDid) { - res.setHeader('Content-Type', 'application/json'); - res.writeHead(403); - - return clearAccountCookie(res).end(JSON.stringify({ - reason: 'the spacedust notifications demo isn\'t public yet!', - })); - } - + const [did, session, _isAdmin] = info; const body = await getRequesBody(req); const { sub } = JSON.parse(body); // addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG) @@ -247,10 +268,10 @@ const handleSubscribe = async (db, req, res, appSecret, adminDid) => { updateSubs(db); res.setHeader('Content-Type', 'application/json'); res.writeHead(201); - res.end('{"oh": "hi"}'); + res.end(JSON.stringify({ sup: 'hi' })); }; -const requestListener = (secrets, jwks, db, adminDid) => (req, res) => { +const requestListener = (secrets, whoamiHost, db, adminDid) => (req, res) => { if (req.method === 'GET' && req.url === '/') { return handleIndex(req, res, { PUBKEY: secrets.pushKeys.publicKey }); } @@ -258,13 +279,21 @@ const requestListener = (secrets, jwks, db, adminDid) => (req, res) => { return handleServiceWorker(req, res, { PUBKEY: secrets.pushKeys.publicKey }); } + if (req.method === 'OPTIONS' && req.url === '/hello') { + return res.writeHead(204, CORS_PERMISSIVE(req)).end(); + } + if (req.method === 'GET' && req.url === '/hello') { + res.setHeaders(new Headers(CORS_PERMISSIVE(req))); + return handleHello(db, req, res, secrets, whoamiHost, adminDid); + } + if (req.method === 'OPTIONS' && req.url === '/verify') { // TODO: probably restrict the origin return res.writeHead(204, CORS_PERMISSIVE(req)).end(); } if (req.method === 'POST' && req.url === '/verify') { res.setHeaders(new Headers(CORS_PERMISSIVE(req))); - return handleVerify(db, req, res, jwks, secrets.appSecret); + return handleVerify(db, req, res, whoamiHost, secrets.appSecret); } if (req.method === 'OPTIONS' && req.url === '/subscribe') { @@ -293,7 +322,6 @@ const main = env => { ); const whoamiHost = env.WHOAMI_HOST ?? 'https://who-am-i.microcosm.blue'; - const jwks = jose.createRemoteJWKSet(new URL(`${whoamiHost}/.well-known/jwks.json`)); const dbFilename = env.DB_FILE ?? './db.sqlite3'; const initDb = process.argv.includes('--init-db'); @@ -307,7 +335,7 @@ const main = env => { const port = parseInt(env.PORT ?? 8000, 10); http - .createServer(requestListener(secrets, jwks, db, adminDid)) + .createServer(requestListener(secrets, whoamiHost, db, adminDid)) .listen(port, host, () => console.log(`listening at http://${host}:${port}`)); };