+72
-39
atproto-notifications/src/components/setup/Chrome.tsx
+72
-39
atproto-notifications/src/components/setup/Chrome.tsx
···
1
import { Link } from 'react-router';
2
import { Handle } from '../User';
3
import './Chrome.css';
4
5
export function Chrome({ user, onLogout, children }) {
6
-
const content = children;
7
-
const logout = () => null;
8
return (
9
<>
10
-
<header id="app-header">
11
-
<h1>
12
-
<Link to="/" className="inherit-font">
13
-
spacedust notifications <span className="demo">demo!</span>
14
-
</Link>
15
-
</h1>
16
-
{user && (
17
-
<div className="current-user">
18
-
<p>
19
-
<span className="handle">
20
-
<Handle did={user.did} />
21
-
{user.role !== 'public' && (
22
-
<span className="chrome-role-tag">
23
-
{user.role === 'admin' ? (
24
-
<Link to="/admin" className="inherit-font">{user.role}</Link>
25
-
) : user.role === 'early' ? (
26
-
<Link to="/early" className="inherit-font">{user.role}</Link>
27
-
) : (
28
-
<>{user.role}</>
29
-
)}
30
-
</span>
31
-
)}
32
-
</span>
33
-
<button className="subtle bad" onClick={onLogout}>×</button>
34
-
</p>
35
-
</div>
36
-
)}
37
-
</header>
38
39
<div id="app-content">
40
-
{content}
41
</div>
42
43
<div className="footer">
···
72
<p className="secret-dev">
73
secret dev setting:
74
{' '}
75
-
<label>
76
-
<input
77
-
type="checkbox"
78
-
onChange={e => setDev(e.target.checked)}
79
-
checked={true /*isDev(ufosHost)*/}
80
-
/>
81
-
localhost
82
-
</label>
83
</p>
84
</div>
85
</>
···
1
+
import { useCallback, useEffect, useState } from 'react';
2
import { Link } from 'react-router';
3
import { Handle } from '../User';
4
+
import { GetJson, postJson } from '../Fetch';
5
import './Chrome.css';
6
7
+
function Header({ user, onLogout }) {
8
+
return (
9
+
<header id="app-header">
10
+
<h1>
11
+
<Link to="/" className="inherit-font">
12
+
spacedust notifications <span className="demo">demo!</span>
13
+
</Link>
14
+
</h1>
15
+
{user && (
16
+
<div className="current-user">
17
+
<p>
18
+
<span className="handle">
19
+
<Handle did={user.did} />
20
+
{user.role !== 'public' && (
21
+
<span className="chrome-role-tag">
22
+
{user.role === 'admin' ? (
23
+
<Link to="/admin" className="inherit-font">{user.role}</Link>
24
+
) : user.role === 'early' ? (
25
+
<Link to="/early" className="inherit-font">{user.role}</Link>
26
+
) : (
27
+
<>{user.role}</>
28
+
)}
29
+
</span>
30
+
)}
31
+
</span>
32
+
<button className="subtle bad" onClick={onLogout}>×</button>
33
+
</p>
34
+
</div>
35
+
)}
36
+
</header>
37
+
);
38
+
}
39
+
40
export function Chrome({ user, onLogout, children }) {
41
+
const [secretDevCounter, setSecretDevCounter] = useState(0);
42
+
const [secretDevStatus, setSecretDevStatus] = useState(null);
43
+
44
+
// ~~is this the best way~~ does it work? yeh
45
+
const setSelfNotify = useCallback(async enabled => {
46
+
setSecretDevStatus('pending');
47
+
const host = import.meta.env.VITE_NOTIFICATIONS_HOST;
48
+
const url = new URL('/global-notify', host);
49
+
try {
50
+
await postJson(url, JSON.stringify({ notify_self: enabled }), true)
51
+
setSecretDevStatus(null);
52
+
} catch (err) {
53
+
console.error('failed to set self-notify setting', err);
54
+
setSecretDevStatus('failed');
55
+
}
56
+
setSecretDevCounter(n => n + 1);
57
+
});
58
+
59
return (
60
<>
61
+
<Header user={user} onLogout={onLogout} />
62
63
<div id="app-content">
64
+
{children}
65
</div>
66
67
<div className="footer">
···
96
<p className="secret-dev">
97
secret dev setting:
98
{' '}
99
+
<GetJson
100
+
key={secretDevCounter}
101
+
endpoint="/global-notify"
102
+
credentials
103
+
loading={() => <>…</>}
104
+
ok={({ notify_self }) => (
105
+
<label>
106
+
<input
107
+
type="checkbox"
108
+
onChange={e => setSelfNotify(e.target.checked)}
109
+
checked={notify_self ^ (secretDevStatus === 'pending')}
110
+
disabled={secretDevStatus === 'pending'}
111
+
/>
112
+
self-notify
113
+
</label>
114
+
)}
115
+
/>
116
</p>
117
</div>
118
</>
+22
-1
server/api.js
+22
-1
server/api.js
···
158
};
159
160
const handleTopSecret = async (db, user, req, res) => {
161
-
console.log('ts');
162
// TODO: succeed early if they're already in?
163
const body = await getRequesBody(req);
164
const { secret_password } = JSON.parse(body);
···
171
return forbidden(res);
172
}
173
};
174
175
const handleListSecrets = async (db, res) => {
176
const secrets = db.getSecrets();
···
265
if (method === 'POST' && pathname === '/super-top-secret-access') {
266
if (!user) return unauthorized(res);
267
return handleTopSecret(db, user, req, res);
268
}
269
270
// non-public access required
···
158
};
159
160
const handleTopSecret = async (db, user, req, res) => {
161
// TODO: succeed early if they're already in?
162
const body = await getRequesBody(req);
163
const { secret_password } = JSON.parse(body);
···
170
return forbidden(res);
171
}
172
};
173
+
174
+
const handleGetGlobalNotifySettings = async (db, user, res) => {
175
+
const settings = db.getNotifyAccountGlobals(user.did);
176
+
return ok(res, settings);
177
+
};
178
+
179
+
const handleSetGlobalNotifySettings = async (db, user, req, res) => {
180
+
const body = await getRequesBody(req);
181
+
const { notify_enabled, notify_self } = JSON.parse(body);
182
+
db.setNotifyAccountGlobals(user.did, { notify_enabled, notify_self });
183
+
return gotIt(res);
184
+
};
185
+
186
+
/// admin stuff
187
188
const handleListSecrets = async (db, res) => {
189
const secrets = db.getSecrets();
···
278
if (method === 'POST' && pathname === '/super-top-secret-access') {
279
if (!user) return unauthorized(res);
280
return handleTopSecret(db, user, req, res);
281
+
}
282
+
if (method === 'GET' && pathname === '/global-notify') {
283
+
if (!user) return unauthorized(res);
284
+
return handleGetGlobalNotifySettings(db, user, res);
285
+
}
286
+
if (method === 'POST' && pathname === '/global-notify') {
287
+
if (!user) return unauthorized(res);
288
+
return handleSetGlobalNotifySettings(db, user, req, res);
289
}
290
291
// non-public access required
+31
server/db.js
+31
server/db.js
···
16
#stmt_delete_push_sub;
17
#stmt_get_push_info;
18
#stmt_set_role;
19
#stmt_admin_add_secret;
20
#stmt_admin_expire_secret;
21
#stmt_admin_get_secrets;
···
111
where did = :did
112
and :secret_password in (select password
113
from top_secret_passwords)`);
114
115
this.#stmt_admin_add_secret = db.prepare(
116
`insert into top_secret_passwords (password)
···
204
let res = this.#stmt_set_role.run(params);
205
return res.changes > 0;
206
}
207
208
addTopSecret(secretPassword) {
209
this.#stmt_admin_add_secret.run(secretPassword);
···
16
#stmt_delete_push_sub;
17
#stmt_get_push_info;
18
#stmt_set_role;
19
+
#stmt_get_notify_account_globals;
20
+
#stmt_set_notify_account_globals;
21
+
22
#stmt_admin_add_secret;
23
#stmt_admin_expire_secret;
24
#stmt_admin_get_secrets;
···
114
where did = :did
115
and :secret_password in (select password
116
from top_secret_passwords)`);
117
+
118
+
this.#stmt_get_notify_account_globals = db.prepare(
119
+
`select notify_enabled,
120
+
notify_self
121
+
from accounts
122
+
where did = :did`);
123
+
124
+
this.#stmt_set_notify_account_globals = db.prepare(
125
+
`update accounts
126
+
set notify_enabled = :notify_enabled,
127
+
notify_self = :notify_self
128
+
where did = :did`);
129
+
130
131
this.#stmt_admin_add_secret = db.prepare(
132
`insert into top_secret_passwords (password)
···
220
let res = this.#stmt_set_role.run(params);
221
return res.changes > 0;
222
}
223
+
224
+
getNotifyAccountGlobals(did) {
225
+
return this.#stmt_get_notify_account_globals.get({ did });
226
+
}
227
+
228
+
setNotifyAccountGlobals(did, globals) {
229
+
this.#transactionally(() => {
230
+
const update = this.getNotifyAccountGlobals(did);
231
+
if (globals.notify_enabled !== undefined) update.notify_enabled = +globals.notify_enabled;
232
+
if (globals.notify_self !== undefined) update.notify_self = +globals.notify_self;
233
+
update.did = did;
234
+
this.#stmt_set_notify_account_globals.run(update);
235
+
});
236
+
}
237
+
238
239
addTopSecret(secretPassword) {
240
this.#stmt_admin_add_secret.run(secretPassword);
+38
-6
server/notifications.js
+38
-6
server/notifications.js
···
84
}
85
};
86
87
const handleDust = db => async event => {
88
console.log('got', event.data);
89
let data;
···
100
}
101
const timestamp = +new Date();
102
103
-
let did;
104
-
if (subject.startsWith('did:')) did = subject;
105
-
else if (subject.startsWith('at://')) {
106
-
const [id, ..._] = subject.slice('at://'.length).split('/');
107
-
if (id.startsWith('did:')) did = id;
108
-
}
109
if (!did) {
110
console.warn(`ignoring link with non-DID subject: ${subject}`)
111
return;
112
}
113
114
const subs = db.getSubsByDid(did);
···
84
}
85
};
86
87
+
const extractUriDid = at_uri => {
88
+
if (!at_uri.startsWith('at://')) {
89
+
console.warn(`ignoring non-at-uri: ${at_uri}`);
90
+
return null;
91
+
}
92
+
const [id, ..._] = at_uri.slice('at://'.length).split('/');
93
+
if (!id) {
94
+
console.warn(`ignoring at-uri with missing id segment: ${at_uri}`);
95
+
return null;
96
+
}
97
+
if (id.startsWith('@')) {
98
+
console.warn(`ignoring @handle at-uri: ${at_uri}`);
99
+
return null;
100
+
}
101
+
if (!id.startsWith('did:')) {
102
+
console.warn(`ignoring non-did at-uri: ${at_uri}`);
103
+
return null;
104
+
}
105
+
return id;
106
+
};
107
+
108
const handleDust = db => async event => {
109
console.log('got', event.data);
110
let data;
···
121
}
122
const timestamp = +new Date();
123
124
+
const did = subject.startsWith('did:') ? subject : extractUriDid(subject);
125
if (!did) {
126
console.warn(`ignoring link with non-DID subject: ${subject}`)
127
return;
128
+
}
129
+
130
+
// this works for now since only the account owner is assumed to be a notification target
131
+
// but for "replies on post" etc that won't hold
132
+
const { notify_enabled, notify_self } = db.getNotifyAccountGlobals(did);
133
+
if (!notify_enabled) console.warn('would drop this since notifies are not enabled (ui todo)');
134
+
if (!notify_self) {
135
+
const source_did = extractUriDid(source_record);
136
+
if (!source_did) {
137
+
console.warn(`ignoring link with non-DID source_record: ${source_record}`)
138
+
return;
139
+
}
140
+
if (source_did === did) {
141
+
console.warn(`ignoring self-notification`);
142
+
return;
143
+
}
144
}
145
146
const subs = db.getSubsByDid(did);
+2
server/schema.sql
+2
server/schema.sql