+69
atproto-notifications/src/components/SecretPassword.jsx
+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…</>)}
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
+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
+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
+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
+4
-2
server/schema.sql