Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/admin/registration-token-hook.tsx
5805 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020-2025 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Custom hook for managing registration tokens.
8
*/
9
10
import { Form } from "antd";
11
import dayjs from "dayjs";
12
import { pick } from "lodash";
13
import { useEffect, useState } from "react";
14
15
import { query } from "@cocalc/frontend/frame-editors/generic/client";
16
import { RegistrationTokenSetFields } from "@cocalc/util/db-schema/types";
17
import { seconds2hms, secure_random_token } from "@cocalc/util/misc";
18
19
import {
20
CUSTOM_PRESET_KEY,
21
EPHEMERAL_OFF_KEY,
22
findPresetKey,
23
type Token,
24
} from "./types";
25
26
export function formatEphemeralHours(value?: number): string {
27
if (value == null) return "";
28
const seconds = value / 1000;
29
return seconds2hms(seconds, false, false, false);
30
}
31
32
export function getEphemeralMode(ephemeral?: number): string | undefined {
33
const presetKey = findPresetKey(ephemeral);
34
if (presetKey) return presetKey;
35
if (ephemeral == null) return EPHEMERAL_OFF_KEY;
36
return CUSTOM_PRESET_KEY;
37
}
38
39
export function useRegistrationTokens() {
40
const [data, setData] = useState<{ [key: string]: Token }>({});
41
const [noOrAllInactive, setNoOrAllInactive] = useState<boolean>(false);
42
const [modalVisible, setModalVisible] = useState<boolean>(false);
43
const [editingToken, setEditingToken] = useState<Token | null>(null);
44
const [saving, setSaving] = useState<boolean>(false);
45
const [deleting, setDeleting] = useState<boolean>(false);
46
const [loading, setLoading] = useState<boolean>(false);
47
const [lastSaved, setLastSaved] = useState<Token | null>(null);
48
const [error, setError] = useState<string>("");
49
const [selRows, setSelRows] = useState<any>([]);
50
const [modalError, setModalError] = useState<string>("");
51
const [licenseInputKey, setLicenseInputKey] = useState<number>(0);
52
53
// Antd
54
const [form] = Form.useForm();
55
56
// we load the data in a map, indexed by the token
57
// dates are converted to dayjs on the fly
58
async function load() {
59
let result: any;
60
setLoading(true);
61
try {
62
// TODO query should be limited by disabled != true
63
result = await query({
64
query: {
65
registration_tokens: {
66
token: "*",
67
descr: null,
68
expires: null,
69
limit: null,
70
disabled: null,
71
ephemeral: null,
72
customize: null,
73
},
74
},
75
});
76
const data = {};
77
let warn_signup = true;
78
for (const x of result.query.registration_tokens) {
79
if (x.expires) x.expires = dayjs(x.expires);
80
x.active = !x.disabled;
81
data[x.token] = x;
82
// we have at least one active token → no need to warn user
83
if (x.active) warn_signup = false;
84
}
85
setNoOrAllInactive(warn_signup);
86
setError("");
87
setData(data);
88
} catch (err) {
89
setError(err.message);
90
} finally {
91
setLoading(false);
92
}
93
}
94
95
useEffect(() => {
96
// every time we show or hide, clear the selection
97
setSelRows([]);
98
load();
99
}, []);
100
101
// saving a specific token value converts dayjs back to pure Date objects
102
// we also record the last saved token as a template for the next add operation
103
async function save(val): Promise<void> {
104
// antd wraps the time in a dayjs object
105
const val_orig: Token = { ...val };
106
107
// data preparation
108
if (val.expires != null && dayjs.isDayjs(val.expires)) {
109
val.expires = dayjs(val.expires).toDate();
110
}
111
val.disabled = !val.active;
112
val = pick(val, [
113
"token",
114
"disabled",
115
"expires",
116
"limit",
117
"descr",
118
"ephemeral",
119
"customize",
120
] as RegistrationTokenSetFields[]);
121
// set optional field to undefined (to get rid of it)
122
["descr", "limit", "expires", "ephemeral"].forEach(
123
(k: RegistrationTokenSetFields) => (val[k] = val[k] ?? undefined),
124
);
125
if (val.customize != null) {
126
const { disableCollaborators, disableAI, disableInternet, license } =
127
val.customize;
128
if (!disableCollaborators && !disableAI && !disableInternet && !license) {
129
val.customize = undefined;
130
}
131
}
132
try {
133
setSaving(true);
134
await query({
135
query: {
136
registration_tokens: val,
137
},
138
timeout: 15000,
139
});
140
// we save the original one, with dayjs in it!
141
setLastSaved(val_orig);
142
setSaving(false);
143
await load();
144
} catch (err) {
145
// Error path - set error (handle non-Error values)
146
const errorMessage = err?.message ?? String(err);
147
setError(errorMessage);
148
throw err; // Re-throw so caller knows it failed
149
} finally {
150
setSaving(false);
151
}
152
}
153
154
async function deleteToken(
155
token: string | undefined,
156
single: boolean = false,
157
) {
158
if (token == null) return;
159
if (single) setDeleting(true);
160
161
try {
162
await query({
163
query: {
164
registration_tokens: { token },
165
},
166
options: [{ delete: true }],
167
});
168
if (single) load();
169
} catch (err) {
170
if (single) {
171
setError(err);
172
} else {
173
throw err;
174
}
175
} finally {
176
if (single) setDeleting(false);
177
}
178
}
179
180
async function deleteTokens(): Promise<void> {
181
setDeleting(true);
182
try {
183
// Delete tokens in parallel and wait for all to complete
184
await Promise.all(selRows.map((token) => deleteToken(token)));
185
setSelRows([]);
186
load();
187
} catch (err) {
188
setError(err);
189
} finally {
190
setDeleting(false);
191
}
192
}
193
194
// we generate a random token and make sure it doesn't exist
195
// TODO also let the user generate one with a validation check
196
function newRandomToken(): string {
197
return secure_random_token(16);
198
}
199
200
// Modal event handlers
201
function handleModalOpen(token?: Token): void {
202
setModalError("");
203
setLicenseInputKey((k) => k + 1); // Force license picker to remount
204
// IMPORTANT: Reset form first to avoid leaking previous values
205
form.resetFields();
206
207
if (token) {
208
// Edit mode
209
const mode = getEphemeralMode(token.ephemeral);
210
form.setFieldsValue({ ...token, _ephemeralMode: mode });
211
setEditingToken(token);
212
} else {
213
// Add mode - use lastSaved as template
214
const newToken = {
215
...lastSaved,
216
token: newRandomToken(),
217
active: true,
218
};
219
const mode = getEphemeralMode(newToken.ephemeral);
220
form.setFieldsValue({ ...newToken, _ephemeralMode: mode });
221
setEditingToken(null);
222
}
223
setModalVisible(true);
224
setLastSaved(null); // Clear last saved marker (mimics old useEffect)
225
}
226
227
function handleModalCancel(): void {
228
setModalVisible(false);
229
setEditingToken(null);
230
setModalError("");
231
form.resetFields();
232
}
233
234
function handleModalReset(): void {
235
setModalError("");
236
setLicenseInputKey((k) => k + 1); // Force license picker to remount
237
// Mimics old Reset button: regenerate token, keep lastSaved template
238
form.resetFields(); // Clear first to avoid stale values
239
const newToken = {
240
...lastSaved,
241
token: newRandomToken(),
242
active: true,
243
};
244
const mode = getEphemeralMode(newToken.ephemeral);
245
form.setFieldsValue({ ...newToken, _ephemeralMode: mode });
246
setEditingToken(null);
247
}
248
249
async function handleModalSave(values: Token): Promise<void> {
250
setModalError("");
251
const val_orig: Token = { ...values };
252
253
try {
254
// Call the existing save() function which handles all transformation and persistence
255
await save(values);
256
257
// Success - close modal
258
setModalVisible(false);
259
setEditingToken(null);
260
} catch (err) {
261
// Error - keep modal open and preserve user input
262
// save() already set the error state, we just need to prevent closing
263
const message = err?.message ?? String(err);
264
setModalError(message);
265
form.setFieldsValue(val_orig); // Restore form with user's values
266
}
267
}
268
269
return {
270
data,
271
form,
272
saving,
273
deleting,
274
deleteToken,
275
deleteTokens,
276
loading,
277
lastSaved,
278
error,
279
setError,
280
selRows,
281
setSelRows,
282
setDeleting,
283
newRandomToken,
284
save,
285
load,
286
noOrAllInactive,
287
// Modal-related
288
modalVisible,
289
editingToken,
290
modalError,
291
licenseInputKey,
292
handleModalOpen,
293
handleModalCancel,
294
handleModalReset,
295
handleModalSave,
296
};
297
}
298
299