+20
-7
atproto-notifications/src/App.tsx
+20
-7
atproto-notifications/src/App.tsx
···
14
</div>
15
);
16
17
-
function requestPermission(host, setAsking) {
18
return async () => {
19
setAsking(true);
20
let err;
···
27
body: JSON.stringify({ sub }),
28
credentials: 'include',
29
});
30
-
if (!res.ok) throw res;
31
} catch (e) {
32
err = e;
33
}
34
setAsking(false);
···
68
const [user, setUser] = useLocalStorage('spacedust-notif-user', null);
69
const [verif, setVerif] = useState(null);
70
const [asking, setAsking] = useState(false);
71
72
const onIdentify = useCallback(async details => {
73
setVerif('verifying');
···
102
content = <><p>Sorry, failed to verify that identity. please let us know!</p>{content}</>;
103
}
104
}
105
-
} else if (notifPerm !== 'granted') {
106
content = (
107
<>
108
<h3>Step 2: Allow notifications</h3>
109
<p>To show notifications we need permission:</p>
110
<p>
111
<button
112
-
onClick={requestPermission(host, setAsking)}
113
disabled={asking}
114
>
115
{asking ? <>Requesting…</> : <>Request permission</>}
···
117
</p>
118
{notifPerm === 'denied' ? (
119
<p className="detail">Notification permission was denied. You may need to clear the browser setting to try again.</p>
120
-
) : (
121
-
<p className="detail">You can revoke this any time</p>
122
-
)}
123
</>
124
);
125
} else {
···
14
</div>
15
);
16
17
+
function requestPermission(host, setAsking, setPermissionError) {
18
return async () => {
19
setAsking(true);
20
let err;
···
27
body: JSON.stringify({ sub }),
28
credentials: 'include',
29
});
30
+
if (!res.ok) {
31
+
let content;
32
+
try {
33
+
content = (await res.json()).reason;
34
+
} catch (_) {
35
+
content = await res.text();
36
+
}
37
+
throw content;
38
+
}
39
} catch (e) {
40
+
setPermissionError(e);
41
err = e;
42
}
43
setAsking(false);
···
77
const [user, setUser] = useLocalStorage('spacedust-notif-user', null);
78
const [verif, setVerif] = useState(null);
79
const [asking, setAsking] = useState(false);
80
+
const [permissionError, setPermissionError] = useState(null);
81
82
const onIdentify = useCallback(async details => {
83
setVerif('verifying');
···
112
content = <><p>Sorry, failed to verify that identity. please let us know!</p>{content}</>;
113
}
114
}
115
+
} else if (permissionError !== null || notifPerm !== 'granted') {
116
content = (
117
<>
118
<h3>Step 2: Allow notifications</h3>
119
<p>To show notifications we need permission:</p>
120
<p>
121
<button
122
+
onClick={requestPermission(host, setAsking, setPermissionError)}
123
disabled={asking}
124
>
125
{asking ? <>Requesting…</> : <>Request permission</>}
···
127
</p>
128
{notifPerm === 'denied' ? (
129
<p className="detail">Notification permission was denied. You may need to clear the browser setting to try again.</p>
130
+
) : permissionError ? (
131
+
<p className="detail">Sorry, something went wrong: {permissionError}</p>
132
+
) : (
133
+
<p className="detail">You can revoke this any time</p>
134
+
)
135
+
}
136
</>
137
);
138
} else {
+5
-2
server/db.js
+5
-2
server/db.js
···
37
38
this.#stmt_insert_account = db.prepare(
39
`insert into accounts (did)
40
-
values (?)`);
41
42
this.#stmt_get_account = db.prepare(
43
`select a.first_seen,
···
53
54
this.#stmt_insert_push_sub = db.prepare(
55
`insert into push_subs (account_did, session, subscription)
56
-
values (?, ?, ?)`);
57
58
this.#stmt_get_all_sub_dids = db.prepare(
59
`select distinct account_did
···
37
38
this.#stmt_insert_account = db.prepare(
39
`insert into accounts (did)
40
+
values (?)
41
+
on conflict do nothing`);
42
43
this.#stmt_get_account = db.prepare(
44
`select a.first_seen,
···
54
55
this.#stmt_insert_push_sub = db.prepare(
56
`insert into push_subs (account_did, session, subscription)
57
+
values (?, ?, ?)
58
+
on conflict do update
59
+
set subscription = excluded.subscription`);
60
61
this.#stmt_get_all_sub_dids = db.prepare(
62
`select distinct account_did
+17
-4
server/index.js
+17
-4
server/index.js
···
225
return res.writeHead(200).end('okayyyy');
226
};
227
228
-
const handleSubscribe = async (db, req, res, appSecret) => {
229
let info = getAccountCookie(req, res, appSecret);
230
if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' }));
231
const [did, session] = info;
232
233
const body = await getRequesBody(req);
234
const { sub } = JSON.parse(body);
235
// addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG)
···
240
res.end('{"oh": "hi"}');
241
};
242
243
-
const requestListener = (secrets, jwks, db) => (req, res) => {
244
if (req.method === 'GET' && req.url === '/') {
245
return handleIndex(req, res, { PUBKEY: secrets.pushKeys.publicKey });
246
}
···
263
}
264
if (req.method === 'POST' && req.url === '/subscribe') {
265
res.setHeaders(new Headers(CORS_PERMISSIVE(req)));
266
-
return handleSubscribe(db, req, res, secrets.appSecret);
267
}
268
269
res.writeHead(200);
···
271
}
272
273
const main = env => {
274
if (!env.SECRETS_FILE) throw new Error('SECRETS_FILE is required to run');
275
const secrets = getOrCreateSecrets(env.SECRETS_FILE);
276
webpush.setVapidDetails(
···
294
const port = parseInt(env.PORT ?? 8000, 10);
295
296
http
297
-
.createServer(requestListener(secrets, jwks, db))
298
.listen(port, host, () => console.log(`listening at http://${host}:${port}`));
299
};
300
···
225
return res.writeHead(200).end('okayyyy');
226
};
227
228
+
const handleSubscribe = async (db, req, res, appSecret, adminDid) => {
229
let info = getAccountCookie(req, res, appSecret);
230
if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' }));
231
const [did, session] = info;
232
233
+
// not yet public!!
234
+
if (did !== adminDid) {
235
+
res.setHeader('Content-Type', 'application/json');
236
+
res.writeHead(403);
237
+
238
+
return clearAccountCookie(res).end(JSON.stringify({
239
+
reason: 'the spacedust notifications demo isn\'t public yet!',
240
+
}));
241
+
}
242
+
243
const body = await getRequesBody(req);
244
const { sub } = JSON.parse(body);
245
// addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG)
···
250
res.end('{"oh": "hi"}');
251
};
252
253
+
const requestListener = (secrets, jwks, db, adminDid) => (req, res) => {
254
if (req.method === 'GET' && req.url === '/') {
255
return handleIndex(req, res, { PUBKEY: secrets.pushKeys.publicKey });
256
}
···
273
}
274
if (req.method === 'POST' && req.url === '/subscribe') {
275
res.setHeaders(new Headers(CORS_PERMISSIVE(req)));
276
+
return handleSubscribe(db, req, res, secrets.appSecret, adminDid);
277
}
278
279
res.writeHead(200);
···
281
}
282
283
const main = env => {
284
+
if (!env.ADMIN_DID) throw new Error('ADMIN_DID is required to run');
285
+
const adminDid = env.ADMIN_DID;
286
+
287
if (!env.SECRETS_FILE) throw new Error('SECRETS_FILE is required to run');
288
const secrets = getOrCreateSecrets(env.SECRETS_FILE);
289
webpush.setVapidDetails(
···
307
const port = parseInt(env.PORT ?? 8000, 10);
308
309
http
310
+
.createServer(requestListener(secrets, jwks, db, adminDid))
311
.listen(port, host, () => console.log(`listening at http://${host}:${port}`));
312
};
313