+76
-90
server/api.js
+76
-90
server/api.js
···
5
import cookieSig from 'cookie-signature';
6
import { v4 as uuidv4 } from 'uuid';
7
8
const getRequesBody = async req => new Promise((resolve, reject) => {
9
let body = '';
10
req.on('data', chunk => body += chunk);
···
24
{ ...COOKIE_BASE, expires: new Date(0) },
25
));
26
27
-
const getAccountCookie = (req, res, appSecret, adminDid, noDidCheck = false) => {
28
const cookies = cookie.parse(req.headers.cookie ?? '');
29
const untrusted = cookies['verified-account'] ?? '';
30
const json = cookieSig.unsign(untrusted, appSecret);
···
40
clearAccountCookie(res);
41
return null;
42
}
43
-
44
-
// not yet public!!
45
-
if (!did || (did !== adminDid && !noDidCheck)) {
46
-
console.log('no, clearing you', did, did === adminDid, noDidCheck);
47
-
clearAccountCookie(res)
48
-
.setHeader('Content-Type', 'application/json')
49
-
.writeHead(403)
50
-
.end(JSON.stringify({
51
-
reason: 'the spacedust notifications demo isn\'t public yet!',
52
-
}));
53
-
throw new Error('unauthorized');
54
}
55
56
-
return [did, session, did && (did === adminDid)];
57
-
};
58
59
// never EVER allow user-controllable input into fname (or just fix the path joining)
60
const handleFile = (fname, ftype) => async (req, res, replace = {}) => {
···
64
content = content.toString();
65
} catch (err) {
66
console.error(err);
67
-
res.writeHead(500);
68
-
res.end('Internal server error');
69
-
return;
70
}
71
res.setHeader('Content-Type', ftype);
72
res.writeHead(200);
···
76
res.end(content);
77
}
78
const handleIndex = handleFile('index.html', 'text/html');
79
-
const handleServiceWorker = handleFile('service-worker.js', 'application/javascript');
80
81
-
const handleHello = async (db, req, res, secrets, whoamiHost, adminDid) => {
82
-
const resBase = { webPushPublicKey: secrets.pushKeys.publicKey, whoamiHost };
83
-
res.setHeader('Content-Type', 'application/json');
84
-
let info = getAccountCookie(req, res, secrets.appSecret, adminDid, true);
85
-
if (info) {
86
-
const [did, _session, isAdmin] = info;
87
-
let role = db.getAccount(did)?.role;
88
-
role = isAdmin ? 'admin' : (role ?? 'public');
89
-
res
90
-
.setHeader('Content-Type', 'application/json')
91
-
.writeHead(200)
92
-
.end(JSON.stringify({ ...resBase, role, did }));
93
-
} else {
94
-
res
95
-
.setHeader('Content-Type', 'application/json')
96
-
.writeHead(200)
97
-
.end(JSON.stringify({ ...resBase, role: 'anonymous' }));
98
-
}
99
-
};
100
-
101
-
const handleVerify = async (db, req, res, secrets, whoamiHost, adminDid, jwks) => {
102
const body = await getRequesBody(req);
103
const { token } = JSON.parse(body);
104
let did;
···
107
did = verified.payload.sub;
108
} catch (e) {
109
console.warn('jwks verification failed', e);
110
-
return clearAccountCookie(res).writeHead(400).end(JSON.stringify({ reason: 'verification failed' }));
111
}
112
const isAdmin = did && did === adminDid;
113
db.addAccount(did);
114
const session = uuidv4();
115
setAccountCookie(res, did, session, secrets.appSecret);
116
-
return res
117
-
.setHeader('Content-Type', 'application/json')
118
-
.writeHead(200)
119
-
.end(JSON.stringify({
120
-
did,
121
role: isAdmin ? 'admin' : 'public',
122
-
webPushPublicKey: secrets.pushKeys.publicKey,
123
-
}));
124
};
125
126
-
const handleSubscribe = async (db, req, res, appSecret, updateSubs, adminDid) => {
127
-
let info = getAccountCookie(req, res, appSecret, adminDid, true);
128
-
if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' }));
129
-
const [did, session, _isAdmin] = info;
130
const body = await getRequesBody(req);
131
const { sub } = JSON.parse(body);
132
-
// addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG)
133
try {
134
-
db.addPushSub(did, session, JSON.stringify(sub));
135
} catch (e) {
136
console.warn('failed to add sub', e);
137
-
return res
138
-
.setHeader('Content-Type', 'application/json')
139
-
.writeHead(500)
140
-
.end(JSON.stringify({ reason: 'failed to register subscription' }));
141
}
142
updateSubs(db);
143
-
res.setHeader('Content-Type', 'application/json');
144
-
res.writeHead(201);
145
-
res.end(JSON.stringify({ sup: 'hi' }));
146
};
147
148
-
const handleLogout = async (db, req, res, appSecret, updateSubs) => {
149
-
let info = getAccountCookie(req, res, appSecret, null, true);
150
-
if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' }));
151
-
const [_did, session, _isAdmin] = info;
152
try {
153
-
db.deleteSub(session);
154
} catch (e) {
155
console.warn('failed to remove sub', e);
156
-
return res
157
-
.setHeader('Content-Type', 'application/json')
158
-
.writeHead(500)
159
-
.end(JSON.stringify({ reason: 'failed to register subscription' }));
160
}
161
updateSubs(db);
162
clearAccountCookie(res);
163
-
res.setHeader('Content-Type', 'application/json');
164
-
res.writeHead(201);
165
-
res.end(JSON.stringify({ sup: 'bye' }));
166
}
167
168
-
const handleTopSecret = async (db, req, res, appSecret) => {
169
-
let info = getAccountCookie(req, res, appSecret, null, true);
170
-
if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' }));
171
-
const [did, _session, _isAdmin] = info;
172
const body = await getRequesBody(req);
173
const { secret_password } = JSON.parse(body);
174
console.log({ secret_password });
175
const role = 'early';
176
-
db.setRole(did, role, secret_password);
177
-
res.setHeader('Content-Type', 'application/json')
178
-
.writeHead(200)
179
-
.end('"heyyy"');
180
}
181
182
const attempt = listener => async (req, res) => {
···
185
return await listener(req, res);
186
} catch (e) {
187
console.error('listener errored:', e);
188
}
189
};
190
···
198
return (req, res) => {
199
res.setHeaders(corsHeaders);
200
if (req.method === 'OPTIONS') {
201
-
return res.writeHead(204).end();
202
}
203
return listener(req, res);
204
}
···
206
207
export const server = (secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, adminDid) => {
208
const handler = (req, res) => {
209
if (req.method === 'GET' && req.url === '/') {
210
-
return handleIndex(req, res, { PUBKEY: secrets.pushKeys.publicKey });
211
-
}
212
-
if (req.method === 'GET' && req.url === '/hello') {
213
-
return handleHello(db, req, res, secrets, whoamiHost, adminDid);
214
}
215
if (req.method === 'POST' && req.url === '/verify') {
216
-
return handleVerify(db, req, res, secrets, whoamiHost, adminDid, jwks);
217
}
218
-
if (req.method === 'POST' && req.url === '/subscribe') {
219
-
return handleSubscribe(db, req, res, secrets.appSecret, updateSubs, adminDid);
220
}
221
if (req.method === 'POST' && req.url === '/logout') {
222
-
return handleLogout(db, req, res, secrets.appSecret, updateSubs);
223
}
224
if (req.method === 'POST' && req.url === '/super-top-secret-access') {
225
return handleTopSecret(db, req, res, secrets.appSecret);
226
}
227
228
-
res.writeHead(404).end('not found (sorry)');
229
};
230
231
return http.createServer(attempt(withCors(allowedOrigin, handler)));
···
5
import cookieSig from 'cookie-signature';
6
import { v4 as uuidv4 } from 'uuid';
7
8
+
const replyJson = (res, code) => res.setHeader('Content-Type', 'application/json').writeHead(code);
9
+
const errJson = (code, reason) => res => replyJson(res, code).end(JSON.stringify({ reason }));
10
+
11
+
const badRequest = (res, reason) => errJson(400, reason)(res);
12
+
const forbidden = errJson(401, 'forbidden');
13
+
const unauthorized = errJson(403, 'unauthorized');
14
+
const notFound = errJson(404, 'not found');
15
+
const serverError = errJson(500, 'internal server error');
16
+
const okBye = res => res.writeHead(204).end();
17
+
const ok = (res, data) => replyJson(res, 200).end(JSON.stringify(data));
18
+
19
const getRequesBody = async req => new Promise((resolve, reject) => {
20
let body = '';
21
req.on('data', chunk => body += chunk);
···
35
{ ...COOKIE_BASE, expires: new Date(0) },
36
));
37
38
+
const getUser = (req, res, db, appSecret, adminDid) => {
39
const cookies = cookie.parse(req.headers.cookie ?? '');
40
const untrusted = cookies['verified-account'] ?? '';
41
const json = cookieSig.unsign(untrusted, appSecret);
···
51
clearAccountCookie(res);
52
return null;
53
}
54
+
let role;
55
+
if (did === adminDid) {
56
+
role = 'admin';
57
+
} else {
58
+
const account = db.getAccount(did);
59
+
if (!account) {
60
+
console.warn('valid account cookie but could not find in db');
61
+
clearAccountCookie(res);
62
+
return null;
63
+
}
64
+
role = account.role ?? 'public';
65
}
66
+
return { did, session, role };
67
+
};
68
69
70
// never EVER allow user-controllable input into fname (or just fix the path joining)
71
const handleFile = (fname, ftype) => async (req, res, replace = {}) => {
···
75
content = content.toString();
76
} catch (err) {
77
console.error(err);
78
+
return serverError(res);
79
}
80
res.setHeader('Content-Type', ftype);
81
res.writeHead(200);
···
85
res.end(content);
86
}
87
const handleIndex = handleFile('index.html', 'text/html');
88
89
+
const handleVerify = async (db, req, res, secrets, jwks, adminDid) => {
90
const body = await getRequesBody(req);
91
const { token } = JSON.parse(body);
92
let did;
···
95
did = verified.payload.sub;
96
} catch (e) {
97
console.warn('jwks verification failed', e);
98
+
return badRequest(res, 'token verification failed');
99
}
100
const isAdmin = did && did === adminDid;
101
db.addAccount(did);
102
const session = uuidv4();
103
setAccountCookie(res, did, session, secrets.appSecret);
104
+
return ok(res, {
105
+
webPushPublicKey: secrets.pushKeys.publicKey,
106
role: isAdmin ? 'admin' : 'public',
107
+
did,
108
+
});
109
};
110
111
+
const handleHello = async (user, req, res, webPushPublicKey, whoamiHost) =>
112
+
ok(res, {
113
+
whoamiHost,
114
+
webPushPublicKey,
115
+
role: user?.role ?? 'anonymous',
116
+
did: user?.did,
117
+
});
118
+
119
+
const handleSubscribe = async (db, user, req, res, updateSubs) => {
120
const body = await getRequesBody(req);
121
const { sub } = JSON.parse(body);
122
try {
123
+
db.addPushSub(user.did, user.session, JSON.stringify(sub));
124
} catch (e) {
125
console.warn('failed to add sub', e);
126
+
return serverError(res);
127
}
128
updateSubs(db);
129
+
return okBye(res);
130
};
131
132
+
const handleLogout = async (db, user, req, res, appSecret, updateSubs) => {
133
try {
134
+
db.deleteSub(user.session);
135
} catch (e) {
136
console.warn('failed to remove sub', e);
137
+
return serverError(res);
138
}
139
updateSubs(db);
140
clearAccountCookie(res);
141
+
return okBye(res);
142
}
143
144
+
const handleTopSecret = async (db, user, req, res) => {
145
const body = await getRequesBody(req);
146
const { secret_password } = JSON.parse(body);
147
console.log({ secret_password });
148
const role = 'early';
149
+
db.setRole(user.did, role, secret_password);
150
+
return okBye(res);
151
}
152
153
const attempt = listener => async (req, res) => {
···
156
return await listener(req, res);
157
} catch (e) {
158
console.error('listener errored:', e);
159
+
return serverError(res);
160
}
161
};
162
···
170
return (req, res) => {
171
res.setHeaders(corsHeaders);
172
if (req.method === 'OPTIONS') {
173
+
return okBye(res);
174
}
175
return listener(req, res);
176
}
···
178
179
export const server = (secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, adminDid) => {
180
const handler = (req, res) => {
181
+
// public (we're doing fall-through auth, what could go wrong)
182
if (req.method === 'GET' && req.url === '/') {
183
+
return handleIndex(req, res, {});
184
}
185
if (req.method === 'POST' && req.url === '/verify') {
186
+
return handleVerify(db, req, res, secrets, jwks, adminDid);
187
}
188
+
189
+
// semi-public
190
+
const user = getUser(req, res, db, secrets.appSecret, adminDid);
191
+
if (req.method === 'GET' && req.url === '/hello') {
192
+
return handleHello(user, req, res, secrets.pushKeys.publicKey, whoamiHost);
193
}
194
+
195
+
// login required
196
if (req.method === 'POST' && req.url === '/logout') {
197
+
if (!user) return unauthorized(res);
198
+
return handleLogout(db, user, req, res, secrets.appSecret, updateSubs);
199
}
200
if (req.method === 'POST' && req.url === '/super-top-secret-access') {
201
+
if (!user) return unauthorized(res);
202
return handleTopSecret(db, req, res, secrets.appSecret);
203
}
204
205
+
// non-public access required
206
+
if (req.method === 'POST' && req.url === '/subscribe') {
207
+
if (!user || user.role === 'public') return forbidden(res);
208
+
return handleSubscribe(db, user, req, res, updateSubs);
209
+
}
210
+
211
+
// admin required
212
+
213
+
// sigh
214
+
return notFound(res);
215
};
216
217
return http.createServer(attempt(withCors(allowedOrigin, handler)));