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.
- package/package.json +6 -6
- package/src/-b-authorities/cloudflareHelpers.ts +11 -2
- package/src/3-path-functions/PathFunctionRunner.ts +168 -97
- package/src/3-path-functions/PathFunctionRunnerMain.ts +8 -2
- package/src/3-path-functions/pathFunctionLoader.ts +11 -6
- package/src/3-path-functions/syncSchema.ts +10 -1
- package/src/4-deploy/edgeBootstrap.ts +10 -1
- package/src/4-querysub/Querysub.ts +77 -3
- package/src/4-querysub/QuerysubController.ts +22 -2
- package/src/4-querysub/permissions.ts +33 -2
- package/src/4-querysub/querysubPrediction.ts +52 -18
- package/src/archiveapps/archiveGCEntry.tsx +38 -0
- package/src/archiveapps/archiveJoinEntry.ts +121 -0
- package/src/archiveapps/archiveMergeEntry.tsx +47 -0
- package/src/archiveapps/compressTest.tsx +59 -0
- package/src/archiveapps/lockTest.ts +127 -0
- package/src/config.ts +5 -0
- package/src/diagnostics/managementPages.tsx +55 -0
- package/src/diagnostics/misc-pages/ArchiveInspect.tsx +325 -0
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +781 -0
- package/src/diagnostics/misc-pages/ArchiveViewerTable.tsx +156 -0
- package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +573 -0
- package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +129 -0
- package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +431 -0
- package/src/diagnostics/misc-pages/RequireAuditPage.tsx +218 -0
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +206 -0
- package/src/diagnostics/misc-pages/TimeRangeView.tsx +648 -0
- package/src/diagnostics/misc-pages/archiveViewerFilter.tsx +221 -0
- package/src/diagnostics/misc-pages/archiveViewerShared.tsx +76 -0
- package/src/email/postmark.tsx +40 -0
- package/src/email/sendgrid.tsx +44 -0
- package/src/functional/UndoWatch.tsx +133 -0
- package/src/functional/diff.ts +858 -0
- package/src/functional/promiseCache.ts +67 -0
- package/src/functional/random.ts +9 -0
- package/src/functional/runCommand.ts +42 -0
- package/src/functional/runOnce.ts +7 -0
- package/src/functional/stats.ts +61 -0
- package/src/functional/throttleRerender.tsx +80 -0
- package/src/library-components/AspectSizedComponent.tsx +88 -0
- package/src/library-components/Histogram.tsx +338 -0
- package/src/library-components/InlinePopup.tsx +67 -0
- package/src/library-components/Notifications.tsx +153 -0
- package/src/library-components/RenderIfVisible.tsx +80 -0
- package/src/library-components/SimpleNotification.tsx +133 -0
- package/src/library-components/TabbedUI.tsx +39 -0
- package/src/library-components/animateAnyElement.tsx +65 -0
- package/src/library-components/errorNotifications.tsx +81 -0
- package/src/library-components/placeholder.ts +18 -0
- package/src/misc/format2.ts +48 -0
- package/src/misc.ts +33 -0
- package/src/misc2.ts +5 -0
- package/src/server.ts +2 -1
- package/src/storage/diskCache.ts +227 -0
- package/src/storage/diskCache2.ts +122 -0
- package/src/storage/fileSystemPointer.ts +72 -0
- package/src/user-implementation/LoginPage.tsx +78 -0
- package/src/user-implementation/RequireAuditPage.tsx +219 -0
- package/src/user-implementation/SecurityPage.tsx +212 -0
- package/src/user-implementation/UserPage.tsx +320 -0
- package/src/user-implementation/addSuperUser.ts +21 -0
- package/src/user-implementation/canSeeSource.ts +41 -0
- package/src/user-implementation/loginEmail.tsx +159 -0
- package/src/user-implementation/setEmailKey.ts +20 -0
- 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
|
+
}
|