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