querysub 0.153.0 → 0.154.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/package.json +6 -6
  2. package/src/-b-authorities/cloudflareHelpers.ts +11 -2
  3. package/src/3-path-functions/PathFunctionRunner.ts +168 -97
  4. package/src/3-path-functions/PathFunctionRunnerMain.ts +8 -2
  5. package/src/3-path-functions/pathFunctionLoader.ts +11 -6
  6. package/src/3-path-functions/syncSchema.ts +10 -1
  7. package/src/4-deploy/edgeBootstrap.ts +10 -1
  8. package/src/4-querysub/Querysub.ts +77 -3
  9. package/src/4-querysub/QuerysubController.ts +22 -2
  10. package/src/4-querysub/permissions.ts +33 -2
  11. package/src/4-querysub/querysubPrediction.ts +52 -18
  12. package/src/archiveapps/archiveGCEntry.tsx +38 -0
  13. package/src/archiveapps/archiveJoinEntry.ts +121 -0
  14. package/src/archiveapps/archiveMergeEntry.tsx +47 -0
  15. package/src/archiveapps/compressTest.tsx +59 -0
  16. package/src/archiveapps/lockTest.ts +127 -0
  17. package/src/config.ts +5 -0
  18. package/src/diagnostics/managementPages.tsx +55 -0
  19. package/src/diagnostics/misc-pages/ArchiveInspect.tsx +325 -0
  20. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +781 -0
  21. package/src/diagnostics/misc-pages/ArchiveViewerTable.tsx +156 -0
  22. package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +573 -0
  23. package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +129 -0
  24. package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +431 -0
  25. package/src/diagnostics/misc-pages/RequireAuditPage.tsx +218 -0
  26. package/src/diagnostics/misc-pages/SnapshotViewer.tsx +206 -0
  27. package/src/diagnostics/misc-pages/TimeRangeView.tsx +648 -0
  28. package/src/diagnostics/misc-pages/archiveViewerFilter.tsx +221 -0
  29. package/src/diagnostics/misc-pages/archiveViewerShared.tsx +76 -0
  30. package/src/email/postmark.tsx +40 -0
  31. package/src/email/sendgrid.tsx +44 -0
  32. package/src/functional/UndoWatch.tsx +133 -0
  33. package/src/functional/diff.ts +858 -0
  34. package/src/functional/promiseCache.ts +67 -0
  35. package/src/functional/random.ts +9 -0
  36. package/src/functional/runCommand.ts +42 -0
  37. package/src/functional/runOnce.ts +7 -0
  38. package/src/functional/stats.ts +61 -0
  39. package/src/functional/throttleRerender.tsx +80 -0
  40. package/src/library-components/AspectSizedComponent.tsx +88 -0
  41. package/src/library-components/Histogram.tsx +338 -0
  42. package/src/library-components/InlinePopup.tsx +67 -0
  43. package/src/library-components/Notifications.tsx +153 -0
  44. package/src/library-components/RenderIfVisible.tsx +80 -0
  45. package/src/library-components/SimpleNotification.tsx +133 -0
  46. package/src/library-components/TabbedUI.tsx +39 -0
  47. package/src/library-components/animateAnyElement.tsx +65 -0
  48. package/src/library-components/errorNotifications.tsx +81 -0
  49. package/src/library-components/placeholder.ts +18 -0
  50. package/src/misc/format2.ts +48 -0
  51. package/src/misc.ts +33 -0
  52. package/src/misc2.ts +5 -0
  53. package/src/server.ts +2 -1
  54. package/src/storage/diskCache.ts +227 -0
  55. package/src/storage/diskCache2.ts +122 -0
  56. package/src/storage/fileSystemPointer.ts +72 -0
  57. package/src/user-implementation/LoginPage.tsx +78 -0
  58. package/src/user-implementation/RequireAuditPage.tsx +219 -0
  59. package/src/user-implementation/SecurityPage.tsx +212 -0
  60. package/src/user-implementation/UserPage.tsx +320 -0
  61. package/src/user-implementation/addSuperUser.ts +21 -0
  62. package/src/user-implementation/canSeeSource.ts +41 -0
  63. package/src/user-implementation/loginEmail.tsx +159 -0
  64. package/src/user-implementation/setEmailKey.ts +20 -0
  65. package/src/user-implementation/userData.ts +974 -0
@@ -0,0 +1,212 @@
1
+ import preact from "preact"; import { qreact } from "../4-dom/qreact";
2
+ import { css } from "../4-dom/css";
3
+ import { TabbedUI } from "../library-components/TabbedUI";
4
+ import { isCurrentUserSuperUser, user_data, user_functions } from "./userData";
5
+ import { sort, timeInDay } from "socket-function/src/misc";
6
+ import { Button } from "../library-components/Button";
7
+ import { viewingUserURL } from "./UserPage";
8
+ import { atomic } from "../2-proxy/PathValueProxyWatcher";
9
+ import { Anchor } from "../library-components/ATag";
10
+ import { InputLabel } from "../library-components/InputLabel";
11
+ import { createURLSync } from "../library-components/URLParam";
12
+ import { redButton } from "../library-components/colors";
13
+
14
+ export class SecurityPage extends qreact.Component {
15
+ render() {
16
+ return <div class={css.pad(10)}>
17
+ <TabbedUI
18
+ tabs={[
19
+ { value: "config", title: "Config", contents: <ConfigTab /> },
20
+ { value: "activityLog", title: "Activity Log", contents: <ActivityLogTab /> },
21
+ { value: "users", title: "Users", contents: <UsersTab /> },
22
+ { value: "blockedIPs", title: "Blocked IPs", contents: <BlockedIPsTab /> },
23
+ ]}
24
+ />
25
+ <style>
26
+ {`
27
+ table > * > * {
28
+ padding: 2px 4px;
29
+ }
30
+ `}
31
+ </style>
32
+ </div>;
33
+ }
34
+ }
35
+
36
+ const pageURL = createURLSync("page", "");
37
+
38
+ class ConfigTab extends qreact.Component {
39
+ render() {
40
+ let signupsOpen = atomic(user_data().secure.signupsOpen);
41
+ return (
42
+ <>
43
+ <div class={css.fontSize(18)}>
44
+ Signups are {signupsOpen
45
+ ? <span class={css.hslcolor(110, 65, 45)}>open</span>
46
+ : <span class={css.hslcolor(0, 0, 0)}>closed</span>
47
+ }
48
+ </div>
49
+ <InputLabel
50
+ label={"Postmark API Key"}
51
+ edit
52
+ editClass={css.ellipsis.maxWidth("100px")}
53
+ value={user_data().secure.postmarkAPIKey}
54
+ onChangeValue={value => user_functions.setPostmarkAPIKey({ apiKey: value })}
55
+ />
56
+ {isCurrentUserSuperUser() && <Button onClick={() => user_functions.test()}>
57
+ test
58
+ </Button>}
59
+ </>
60
+ );
61
+ }
62
+ }
63
+ const filterStartTimeURL = createURLSync("starttime", 0);
64
+ const filterEndTimeURL = createURLSync("endtime", 0);
65
+ class ActivityLogTab extends qreact.Component {
66
+ render() {
67
+ let startTime = filterStartTimeURL.value || (Date.now() - timeInDay);
68
+ let endTime = filterEndTimeURL.value || (Date.now() + timeInDay);
69
+ let activityEntries = Object.entries(user_data().secure.activityByChunk);
70
+ activityEntries = activityEntries.filter(([timeStr]) => {
71
+ let time = +timeStr;
72
+ return startTime <= time && time <= endTime;
73
+ });
74
+ let activities = activityEntries.flatMap(([_, activities]) => Object.values(activities));
75
+ sort(activities, x => -x.time);
76
+ return (
77
+ <div class={css.vbox(10)}>
78
+ <div class={css.hbox(10)}>
79
+ <InputLabel
80
+ label="Start Time"
81
+ value={new Date(startTime).toISOString().slice(0, 10)}
82
+ onChange={e => filterStartTimeURL.value = new Date(e.currentTarget.value).getTime()}
83
+ />
84
+ <InputLabel
85
+ label="End Time"
86
+ value={new Date(endTime).toISOString().slice(0, 10)}
87
+ onChange={e => filterEndTimeURL.value = new Date(e.currentTarget.value).getTime()}
88
+ />
89
+ </div>
90
+ <div class={css.vbox(10)}>
91
+ <table>
92
+ <tr>
93
+ <th>Time</th>
94
+ <th>Type</th>
95
+ <th>Machine</th>
96
+ <th>User</th>
97
+ <th>IP</th>
98
+ <th>Actions</th>
99
+ <th>Fields</th>
100
+ </tr>
101
+ {activities.map(activity => (
102
+ <tr>
103
+ <td>{new Date(activity.time).toISOString()}</td>
104
+ <td>{activity.type}</td>
105
+ <td>{activity.machineId}</td>
106
+ <td>{activity.userId}</td>
107
+ <td>{activity.ip}</td>
108
+ <td>
109
+ <div class={css.hbox(10)}>
110
+ {!(activity.ip in user_data().secure.blockedIPs) && <Button
111
+ class={css.hsl(0, 75, 50).bord(1, "hsl(0, 75%, 75%)")}
112
+ onClick={() => user_functions.specialBlockIP({ ip: activity.ip })}
113
+ >
114
+ Block IP
115
+ </Button>}
116
+ {(activity.ip in user_data().secure.blockedIPs) && <Button
117
+ class={css.hsl(0, 75, 50).bord(1, "hsl(0, 75%, 75%)")}
118
+ onClick={() => user_functions.specialUnblockIP({ ip: activity.ip })}
119
+ >
120
+ Unblock IP
121
+ </Button>}
122
+ <Button onClick={() => {
123
+ let fields = JSON.parse(activity.fieldsJSON);
124
+ let userId = fields.userId;
125
+ if (!userId) throw new Error("No userId in fields");
126
+ pageURL.value = "user";
127
+ viewingUserURL.value = userId;
128
+ }}>
129
+ Go To User
130
+ </Button>
131
+ </div>
132
+ </td>
133
+ <td>{activity.fieldsJSON}</td>
134
+ </tr>
135
+ ))}
136
+ </table>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+ }
142
+ class UsersTab extends qreact.Component {
143
+ render() {
144
+ let users = Object.entries(user_data().users);
145
+ sort(users, x => -(x[1].lastSignInTime || 0));
146
+ return (
147
+ <div class={css.vbox(10)}>
148
+ <table>
149
+ <tr>
150
+ <th>User Id</th>
151
+ <th>Display Name</th>
152
+ <th>Email</th>
153
+ <th>User Type</th>
154
+ <th>Create Time</th>
155
+ <th>Last Sign In</th>
156
+ </tr>
157
+ {users.map(([userId, user]) => (
158
+ <tr>
159
+ <td>
160
+ <Anchor values={[{ param: pageURL, value: "user" }, { param: viewingUserURL, value: userId }]}>
161
+ {userId}
162
+ </Anchor>
163
+ </td>
164
+ <td>{user.settings.displayName}</td>
165
+ <td>{user.email || <span class={redButton}>Banned</span>}</td>
166
+ <td>{user.userType}</td>
167
+ <td>{new Date(user.createTime).toLocaleString()}</td>
168
+ <td>{user.lastSignInTime && new Date(user.lastSignInTime).toLocaleString()}</td>
169
+ </tr>
170
+ ))}
171
+ </table>
172
+ </div>
173
+ );
174
+ }
175
+ }
176
+ class BlockedIPsTab extends qreact.Component {
177
+ render() {
178
+ let blockedIPs = Object.entries(user_data().secure.blockedIPs);
179
+ sort(blockedIPs, x => -x[1].addedTime);
180
+ return (
181
+ <div class={css.vbox(10)}>
182
+ <InputLabel
183
+ label="Block IP"
184
+ onChangeValue={ip => user_functions.specialBlockIP({ ip })}
185
+ />
186
+ <table>
187
+ <tr>
188
+ <th>IP</th>
189
+ <th>Added Time</th>
190
+ <th>Added By</th>
191
+ <th>Actions</th>
192
+ </tr>
193
+ {blockedIPs.map(([ip, blocked]) => (
194
+ <tr>
195
+ <td>{ip}</td>
196
+ <td>{new Date(blocked.addedTime).toLocaleString()}</td>
197
+ <td>{blocked.addedByUserId}</td>
198
+ <td>
199
+ <Button
200
+ class={css.hsl(0, 75, 50).bord(1, "hsl(0, 75%, 75%)")}
201
+ onClick={() => user_functions.specialUnblockIP({ ip })}
202
+ >
203
+ Unblock IP
204
+ </Button>
205
+ </td>
206
+ </tr>
207
+ ))}
208
+ </table>
209
+ </div>
210
+ );
211
+ }
212
+ }
@@ -0,0 +1,320 @@
1
+ import preact from "preact"; import { qreact } from "../4-dom/qreact";
2
+ import { LOCAL_STORAGE_COMMIT_DELAY_KEY, UserType, getUserObj, getUserObjAssert, isCurrentUserSuperUser, user, userAssert, user_data, user_functions } from "./userData";
3
+ import { css } from "../4-dom/css";
4
+ import { getOwnMachineId } from "../-a-auth/certs";
5
+ import { ellipsis } from "../misc";
6
+ import { sort } from "socket-function/src/misc";
7
+ import { Button } from "../library-components/Button";
8
+ import { atomic } from "../2-proxy/PathValueProxyWatcher";
9
+ import { generateLoginEmail } from "./loginEmail";
10
+ import { list } from "socket-function/src/misc";
11
+ import { Querysub } from "../4-querysub/Querysub";
12
+ import { isDefined } from "../misc";
13
+ import { cacheJSONArgsEqual } from "socket-function/src/caching";
14
+ import { Anchor } from "../library-components/ATag";
15
+ import { InputLabel, InputLabelURL } from "../library-components/InputLabel";
16
+ import { createURLSync } from "../library-components/URLParam";
17
+ import { greenButton, redButton, yellowButton } from "../library-components/colors";
18
+ import { ButtonSelector } from "../library-components/ButtonSelector";
19
+ import { DropdownSelector } from "../library-components/DropdownSelector";
20
+ import { renderToString } from "../library-components/renderToString";
21
+
22
+ // TODO: Support registering arbitrary configuration
23
+ // - For variables (value, type, description)
24
+ // - With arbitrary UI
25
+ // import { imageQualitySTORAGE, skipDevicePixelRatioSTORAGE } from "../framework-beta/Image2";
26
+ /*
27
+ <ButtonSelector
28
+ title="Image Quality"
29
+ value={imageQualitySTORAGE.value}
30
+ onChange={v => imageQualitySTORAGE.value = v}
31
+ options={[
32
+ isCurrentUserSuperUser() ? { value: 100, title: "Raw (May lag the server)" } : undefined,
33
+ { value: 99, title: "Max Quality" },
34
+ { value: 90, title: "Normal" },
35
+ { value: 50, title: "Degraded" },
36
+ { value: 10, title: "Potato" },
37
+ ].filter(isDefined)}
38
+ />
39
+
40
+ {window.devicePixelRatio !== 1 && <InputLabelURL
41
+ label="Use Lower Resolution Images"
42
+ checkbox
43
+ url={skipDevicePixelRatioSTORAGE}
44
+ />}
45
+ */
46
+
47
+
48
+ export const viewingUserURL = createURLSync("viewinguser", "");
49
+ export class UserPage extends qreact.Component {
50
+ state = {
51
+ inviteUser: "",
52
+ };
53
+ render() {
54
+ let realUserId = user();
55
+ let userId = viewingUserURL.value || realUserId;
56
+ let isViewingOther = userId !== realUserId;
57
+
58
+ let userObj = user_data().users[userId];
59
+ let isSuper = isCurrentUserSuperUser();
60
+
61
+ const ownMachineId = getOwnMachineId();
62
+
63
+ return (
64
+ <div class={css.pad(16, 20).fillWidth.overflow("auto")}>
65
+ <style>
66
+ {`
67
+ table > * > * {
68
+ padding: 2px 4px;
69
+ }
70
+ `}
71
+ </style>
72
+ <div class={css.vbox(20)}>
73
+ <div class={css.fontSize(28)}>User Configuration {isViewingOther && <span>({userId})</span>}</div>
74
+ {userObj.userId !== userId && <div class={css.fontSize(16)}>(Used does not exist)</div>}
75
+ <div class={css.vbox(4)}>
76
+ <div><b>Email</b> {userObj.email}</div>
77
+ {isViewingOther && atomic(userObj.email) &&
78
+ <Button class={redButton} onClick={() => user_functions.banUser({ userId })}>
79
+ Ban User
80
+ </Button>
81
+ }
82
+ {isViewingOther && !atomic(userObj.email) &&
83
+ <Button class={greenButton} onClick={() => user_functions.unbanUser({ userId })}>
84
+ Unban User
85
+ </Button>
86
+ }
87
+ <InputLabel
88
+ label="Display Name"
89
+ value={userObj.settings.displayName}
90
+ onChange={e => user_functions.updateSettings({ displayName: e.currentTarget.value })}
91
+ />
92
+ {isSuper &&
93
+ <DropdownSelector<UserType>
94
+ value={userObj.userType}
95
+ options={[
96
+ { value: "user", label: "User" },
97
+ { value: "moderator", label: "Moderator" },
98
+ { value: "admin", label: "Admin" },
99
+ { value: "superuser", label: "Superuser" },
100
+ ]}
101
+ onChange={v => user_functions.specialSetUserType({ userId, userType: v })}
102
+ />
103
+ }
104
+ <div>Account created on {new Date(userObj.createTime).toLocaleString()}</div>
105
+ <div>Last signed in {new Date(userObj.lastSignInTime || 0).toLocaleString()}</div>
106
+ <InputLabel
107
+ label="Commit delay time (ms)"
108
+ number
109
+ value={Querysub.DELAY_COMMIT_DELAY}
110
+ onChange={e => {
111
+ Querysub.DELAY_COMMIT_DELAY = Number(e.currentTarget.value);
112
+ localStorage.setItem(LOCAL_STORAGE_COMMIT_DELAY_KEY, Querysub.DELAY_COMMIT_DELAY.toString());
113
+ Querysub.onCommitFinished(() => {
114
+ this.forceUpdate();
115
+ });
116
+ }}
117
+ />
118
+ </div>
119
+ {!isViewingOther && <Button class={yellowButton} onClick={() => {
120
+ user_functions.logoutSpecific({ machineId: Object.keys(userObj.machineIds), ips: Object.keys(userObj.allowedIPs) });
121
+ }}>Logout All Devices (Requires immediately logging back in)</Button>}
122
+ {isViewingOther && <Button class={yellowButton} onClick={() => {
123
+ user_functions.specialResetLogins({ userId });
124
+ }}>Logout All Devices</Button>}
125
+ <div>
126
+ <div class={css.fontSize(20)}>Authenticated Machines</div>
127
+ <table>
128
+ <tr>
129
+ <th>Machine ID</th>
130
+ <th>Time</th>
131
+ <th>IP</th>
132
+ <th>Actions</th>
133
+ </tr>
134
+ {sort(Object.entries(userObj.machineIds), x => -x[1].time).map(([machineId, machineData]) => {
135
+ return (
136
+ <tr>
137
+ <td>{machineId} {machineId === ownMachineId && <span class={css.hslcolor(120, 75, 75)}>(current machine)</span>}</td>
138
+ <td>{new Date(machineData.time).toLocaleString()}</td>
139
+ <td>{machineData.ip}</td>
140
+ <td>
141
+ {!isViewingOther && <Button class={yellowButton} onClick={() => user_functions.logoutSpecific({ machineId: [machineId], ips: [] })}>
142
+ Remove
143
+ </Button>}
144
+ </td>
145
+ </tr>
146
+ );
147
+ })}
148
+ </table>
149
+ </div>
150
+ <div>
151
+ <div class={css.fontSize(20)}>Allowed IPs</div>
152
+ <table>
153
+ <tr>
154
+ <th>IP</th>
155
+ <th>Time</th>
156
+ <th>Machine ID</th>
157
+ <th>Actions</th>
158
+ </tr>
159
+ {sort(Object.entries(userObj.allowedIPs), x => -x[1].time).map(([ip, ipData]) => {
160
+ return (
161
+ <tr>
162
+ <td>{ip}</td>
163
+ <td>{new Date(ipData.time).toLocaleString()}</td>
164
+ <td>{ipData.machineId}</td>
165
+ <td>
166
+ <div class={css.hbox(6)}>
167
+ {!isViewingOther && <Button class={yellowButton} onClick={() => user_functions.logoutSpecific({ machineId: [], ips: [ip] })}>
168
+ Remove
169
+ </Button>}
170
+ {isViewingOther &&
171
+ <>
172
+ {!(ip in user_data().secure.blockedIPs) && <Button
173
+ class={yellowButton}
174
+ onClick={() => user_functions.specialBlockIP({ ip })}
175
+ >
176
+ Block IP
177
+ </Button>}
178
+ {(ip in user_data().secure.blockedIPs) && <Button
179
+ class={yellowButton}
180
+ onClick={() => user_functions.specialUnblockIP({ ip })}
181
+ >
182
+ Unblock IP
183
+ </Button>}
184
+ </>
185
+ }
186
+ </div>
187
+ </td>
188
+ </tr>
189
+ );
190
+ })}
191
+ </table>
192
+ </div>
193
+ <div>
194
+ <div class={css.fontSize(20)}>Outstanding Login Emails</div>
195
+ <table>
196
+ <tr>
197
+ {isSuper && <th>Login Token</th>}
198
+ <th>Create Time</th>
199
+ <th>Timeout Time</th>
200
+ <th>IP</th>
201
+ <th>Machine ID</th>
202
+ <th>Redirect URL</th>
203
+ <th>Actions</th>
204
+ </tr>
205
+ {sort(Object.entries(userObj.loginTokens), x => -x[1].createTime).map(([loginToken, loginTokenData]) => {
206
+ return (
207
+ <tr>
208
+ {isSuper && <td>{ellipsis(loginToken, 40)}</td>}
209
+ <td>{new Date(loginTokenData.createTime).toLocaleString()}</td>
210
+ <td>{new Date(loginTokenData.timeoutTime).toLocaleString()}</td>
211
+ <td>{loginTokenData.ip}</td>
212
+ <td>{loginTokenData.machineId}</td>
213
+ <td>{loginTokenData.redirectURL}</td>
214
+ <td>
215
+ {!isViewingOther && <Button class={yellowButton} onClick={() => user_functions.removeLoginToken({ loginToken: loginToken })}>
216
+ Remove
217
+ </Button>}
218
+ </td>
219
+ </tr>
220
+ );
221
+ })}
222
+ </table>
223
+ </div>
224
+ <div>
225
+ <div class={css.fontSize(20)}>Added Users</div>
226
+ {!isViewingOther && <div class={css.hbox(10).padv(6)}>
227
+ <InputLabel
228
+ label="User Email"
229
+ value={this.state.inviteUser}
230
+ onChangeValue={value => {
231
+ console.log({ value });
232
+ this.state.inviteUser = value;
233
+ }}
234
+ />
235
+ <Button onClick={() => {
236
+ user_functions.inviteUser({ email: this.state.inviteUser });
237
+ }}>Add User</Button>
238
+ </div>}
239
+ {isSuper &&
240
+ <div class={css.hbox(10).padv(6)}>
241
+ <InputLabel
242
+ label="Adds Remaining"
243
+ number
244
+ value={userObj.invitesRemaining}
245
+ onChange={e => {
246
+ user_functions.specialSetInviteCount({ userId, count: Number(e.currentTarget.value) });
247
+ }}
248
+ />
249
+ </div>
250
+ }
251
+ <table>
252
+ <tr>
253
+ <th>User ID</th>
254
+ <th>Time</th>
255
+ </tr>
256
+ {sort(Object.entries(userObj.invitedUsers), x => -x[1].time).map(([userId, inviteData]) => {
257
+ return (
258
+ <tr>
259
+ <td>
260
+ <Anchor values={[{ param: viewingUserURL, value: userId }]}>
261
+ {userId}
262
+ </Anchor>
263
+ </td>
264
+ <td>{inviteData.time}</td>
265
+ </tr>
266
+ );
267
+ })}
268
+ </table>
269
+ </div>
270
+ <div>
271
+ <div class={css.fontSize(20)}>Latest Page Load By Machine/IP</div>
272
+ <table>
273
+ <tr>
274
+ <th>Source</th>
275
+ <th>Time</th>
276
+ <th>Machine ID</th>
277
+ <th>IP</th>
278
+ </tr>
279
+ {sort(Object.entries(userObj.lastPageLoadsIPs), x => -x[1].time).map(([ipOrMachineId, ipData]) => {
280
+ return (
281
+ <tr>
282
+ {ipOrMachineId}
283
+ <td>{new Date(ipData.time).toLocaleString()}</td>
284
+ <td>{ipData.machineId}</td>
285
+ <td>{ipData.ip}</td>
286
+ </tr>
287
+ );
288
+ })}
289
+ </table>
290
+ </div>
291
+ {isCurrentUserSuperUser() &&
292
+ <div>
293
+ <h2>Example Login Email</h2>
294
+ <div key="email" ref2={e => {
295
+ e.innerHTML = renderToString(generateLoginEmail({
296
+ userId,
297
+ loginToken: "loginToken",
298
+ ip: "127.0.0.1",
299
+ machineId: getOwnMachineId(),
300
+ redirectURL: location.href,
301
+ noHTMLWrapper: true,
302
+ timeoutTime: Date.now() + 1000 * 60 * 60,
303
+ }).contents);
304
+ }} />
305
+ {/* {generateLoginEmail({
306
+ userId,
307
+ loginToken: "loginToken",
308
+ ip: "127.0.0.1",
309
+ machineId: getOwnMachineId(),
310
+ redirectURL: location.href,
311
+ noHTMLWrapper: true,
312
+ timeoutTime: Date.now() + 1000 * 60 * 60,
313
+ }).contents} */}
314
+ </div>
315
+ }
316
+ </div>
317
+ </div>
318
+ );
319
+ }
320
+ }
@@ -0,0 +1,21 @@
1
+ import { Querysub } from "../4-querysub/Querysub";
2
+ import { scriptCreateUser } from "./userData";
3
+ import { pathValueCommitter } from "../0-path-value-core/PathValueCommitter";
4
+
5
+ async function main() {
6
+ const howToCall = ` Call with yarn addsuperuser <email>`;
7
+ const args = process.argv.slice(2).filter(x => !x.startsWith("--"));
8
+ const userId = args[0];
9
+ console.log(process.argv);
10
+ if (!userId) throw new Error("No userId provided." + howToCall);
11
+ if (!userId.includes("@")) throw new Error(`Invalid email ${userId}.` + howToCall);
12
+
13
+ await Querysub.hostService("addSuperUser");
14
+
15
+ await Querysub.commitSynced(() => {
16
+ scriptCreateUser({ userId });
17
+ });
18
+
19
+ await pathValueCommitter.waitForValuesToCommit();
20
+ }
21
+ main().catch(e => console.log(e)).finally(() => process.exit());
@@ -0,0 +1,41 @@
1
+ import { MachineSourceCheck, Querysub } from "../4-querysub/Querysub";
2
+ import { user_data, type LOCAL_STORAGE_USER_ID_KEY, satisifiedUserType, type LOCAL_STORAGE_USER_TYPE_KEY } from "./userData";
3
+
4
+ // TODO: Move this to use the management user instead?
5
+ // - The management user fits better, and should work for more cases.
6
+ export function createSourceCheck(): MachineSourceCheck<{ userId: string | null }> {
7
+ return {
8
+ async shouldMachineIdSeeSource(config) {
9
+ const userId = config.extra.userId;
10
+ if (!userId) {
11
+ return false;
12
+ }
13
+ return await Querysub.syncedCommit(() => {
14
+ let userObj = user_data().users[userId];
15
+ // NOTE: We only check machineId, not IP. This is a less secure check than our login check,
16
+ // but it is fine, as this only hides the sourcemaps, which don't really contain anything surprising.
17
+ if (!(config.machineId in userObj.machineIds)) {
18
+ return false;
19
+ }
20
+ let allowed = satisifiedUserType(userObj.userType, "admin");
21
+ return allowed;
22
+ });
23
+ },
24
+ getExtraDataClientsideInline() {
25
+ if (new URL(location.href).searchParams.has("dropto")) {
26
+ return "nosource";
27
+ }
28
+ const keyType: typeof LOCAL_STORAGE_USER_TYPE_KEY = "userType";
29
+ let lastType = localStorage.getItem(keyType);
30
+ // HACK: We hardcode admin and superuser here. If we add new types that need to see source,
31
+ // we will need to add them here.
32
+ if (lastType !== "admin" && lastType !== "superuser") {
33
+ return "nosource";
34
+ }
35
+
36
+ const key: typeof LOCAL_STORAGE_USER_ID_KEY = "userId";
37
+ let userId = localStorage.getItem(key);
38
+ return { userId, };
39
+ },
40
+ };
41
+ }