@strapi-community/plugin-better-auth-dashboard 1.0.0-beta.1 → 1.0.0-beta.2

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 (45) hide show
  1. package/dist/admin/Avatar-CdT7eBCh.js +39 -0
  2. package/dist/admin/Avatar-zlMTAZh3.mjs +40 -0
  3. package/dist/admin/{Root-2FMy-qQg.mjs → Root-CgVBJuoM.mjs} +12 -4
  4. package/dist/admin/{Root-DDi0mtve.js → Root-CsiOnzlB.js} +12 -4
  5. package/dist/admin/cs-C_jSgeON.js +416 -0
  6. package/dist/admin/cs-wtnknvBC.mjs +416 -0
  7. package/dist/admin/de-Cg5HyoCY.js +416 -0
  8. package/dist/admin/de-DnnVdUxk.mjs +416 -0
  9. package/dist/admin/en-B3utEgMT.mjs +416 -0
  10. package/dist/admin/en-Cz1IJEvp.js +416 -0
  11. package/dist/admin/es-CGA0Tslt.js +416 -0
  12. package/dist/admin/es-RoCEriJL.mjs +416 -0
  13. package/dist/admin/fr-Ca74t21t.js +416 -0
  14. package/dist/admin/fr-CtmuSfiQ.mjs +416 -0
  15. package/dist/admin/{index-BHfYu0uA.js → index-BCBu-0nr.js} +65 -10
  16. package/dist/admin/{index-D9CiI8Yr.mjs → index-BI2WIsds.mjs} +73 -18
  17. package/dist/admin/index-CPSoAbZt.js +2321 -0
  18. package/dist/admin/index-D9ttdrT3.js +2716 -0
  19. package/dist/admin/index-DQdn5s8q.js +1184 -0
  20. package/dist/admin/index-DvfwvCj0.mjs +1182 -0
  21. package/dist/admin/index-VyHXoQ6p.mjs +2321 -0
  22. package/dist/admin/index-gFqHlMLP.mjs +2716 -0
  23. package/dist/admin/index.js +1 -1
  24. package/dist/admin/index.mjs +1 -1
  25. package/dist/admin/it-BAB9hDdp.mjs +416 -0
  26. package/dist/admin/it-dYT2_ERV.js +416 -0
  27. package/dist/admin/nl-CVqSbUU9.js +416 -0
  28. package/dist/admin/nl-CYv26PHv.mjs +416 -0
  29. package/dist/admin/pl-Bc0cS2mD.mjs +416 -0
  30. package/dist/admin/pl-HYF4BxRv.js +416 -0
  31. package/dist/admin/pt-DDUGyLtb.js +416 -0
  32. package/dist/admin/pt-DZyyK3EG.mjs +416 -0
  33. package/dist/admin/{useModelSchema-CqOTI7P1.js → useModelSchema-BogQah8f.js} +309 -287
  34. package/dist/admin/{useModelSchema-B2tmzlwQ.mjs → useModelSchema-C6bxNjW4.mjs} +320 -298
  35. package/dist/server/index.js +15 -15
  36. package/dist/server/index.mjs +15 -15
  37. package/package.json +4 -4
  38. package/dist/admin/Avatar-Cvk89aMx.js +0 -67
  39. package/dist/admin/Avatar-DNczJU0i.mjs +0 -66
  40. package/dist/admin/index-BQi_BUi6.mjs +0 -1968
  41. package/dist/admin/index-BV-4M_Fp.js +0 -1970
  42. package/dist/admin/index-CANDPUGD.mjs +0 -1215
  43. package/dist/admin/index-CqR8WpOa.js +0 -1217
  44. package/dist/admin/index-D6ktNZcj.js +0 -1685
  45. package/dist/admin/index-DszX3ibA.mjs +0 -1683
@@ -1,1970 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const jsxRuntime = require("react/jsx-runtime");
4
- const designSystem = require("@strapi/design-system");
5
- const icons = require("@strapi/icons");
6
- const admin = require("@strapi/strapi/admin");
7
- const react = require("react");
8
- const reactQuery = require("react-query");
9
- const styled = require("styled-components");
10
- const index = require("./index-BHfYu0uA.js");
11
- const Avatar = require("./Avatar-Cvk89aMx.js");
12
- const useModelSchema = require("./useModelSchema-CqOTI7P1.js");
13
- const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
14
- const styled__default = /* @__PURE__ */ _interopDefault(styled);
15
- function useUsers(options = {}) {
16
- const { limit = 25, offset = 0, search } = options;
17
- return reactQuery.useQuery({
18
- queryKey: ["dash-users", limit, offset, search],
19
- queryFn: async () => {
20
- const whereClause = search ? JSON.stringify([
21
- {
22
- field: "email",
23
- operator: "contains",
24
- value: search,
25
- connector: "OR"
26
- },
27
- {
28
- field: "name",
29
- operator: "contains",
30
- value: search,
31
- connector: "OR"
32
- }
33
- ]) : void 0;
34
- const result = await index.client.dash.listUsers({
35
- query: {
36
- limit,
37
- offset,
38
- sortBy: "createdAt",
39
- sortOrder: "desc",
40
- ...whereClause ? { where: whereClause } : {}
41
- }
42
- });
43
- if (result.error) {
44
- throw new Error(result.error.message ?? "Failed to load users");
45
- }
46
- return result.data;
47
- },
48
- keepPreviousData: true
49
- });
50
- }
51
- function CreateUserDialog({ onClose }) {
52
- const qc = reactQuery.useQueryClient();
53
- const { toggleNotification } = admin.useNotification();
54
- const [name, setName] = react.useState("");
55
- const [email, setEmail] = react.useState("");
56
- const [image, setImage] = react.useState("");
57
- const [password, setPassword] = react.useState("");
58
- const [generatePassword, setGeneratePassword] = react.useState(false);
59
- const [emailVerified, setEmailVerified] = react.useState(false);
60
- const [sendVerificationEmail, setSendVerificationEmail] = react.useState(false);
61
- const createMutation = reactQuery.useMutation({
62
- mutationFn: async () => {
63
- const result = await index.client.dash.createUser({
64
- name,
65
- email,
66
- ...image ? { image } : {},
67
- ...generatePassword ? { generatePassword: true } : password ? { password } : {},
68
- emailVerified,
69
- sendVerificationEmail
70
- });
71
- if (result.error)
72
- throw new Error(result.error.message ?? "Create failed");
73
- return result.data;
74
- },
75
- onSuccess: () => {
76
- qc.invalidateQueries({ queryKey: ["dash-users"] });
77
- qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
78
- toggleNotification({
79
- type: "success",
80
- message: "User created successfully"
81
- });
82
- onClose();
83
- },
84
- onError: (err) => {
85
- toggleNotification({
86
- type: "danger",
87
- message: err.message ?? "Failed to create user"
88
- });
89
- }
90
- });
91
- const footer = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
92
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: onClose, children: "Cancel" }),
93
- /* @__PURE__ */ jsxRuntime.jsx(
94
- designSystem.Button,
95
- {
96
- loading: createMutation.isLoading,
97
- onClick: () => createMutation.mutate(),
98
- disabled: !name || !email,
99
- "data-testid": "create-user-submit",
100
- children: "Create user"
101
- }
102
- )
103
- ] });
104
- return /* @__PURE__ */ jsxRuntime.jsx(
105
- useModelSchema.Drawer,
106
- {
107
- title: "Create user",
108
- footer,
109
- onClose,
110
- "data-testid": "create-user-drawer",
111
- children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, children: [
112
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
113
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Personal information" }),
114
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { style: { width: "100%" }, required: true, children: [
115
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Full name" }),
116
- /* @__PURE__ */ jsxRuntime.jsx(
117
- designSystem.TextInput,
118
- {
119
- name: "name",
120
- value: name,
121
- onChange: (e) => setName(e.target.value),
122
- placeholder: "Jane Smith",
123
- "data-testid": "new-user-name"
124
- }
125
- )
126
- ] }),
127
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { style: { width: "100%" }, required: true, children: [
128
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Email address" }),
129
- /* @__PURE__ */ jsxRuntime.jsx(
130
- designSystem.TextInput,
131
- {
132
- name: "email",
133
- type: "email",
134
- value: email,
135
- onChange: (e) => setEmail(e.target.value),
136
- placeholder: "jane@example.com",
137
- "data-testid": "new-user-email"
138
- }
139
- )
140
- ] }),
141
- /* @__PURE__ */ jsxRuntime.jsx(
142
- useModelSchema.MediaPickerField,
143
- {
144
- label: "Avatar URL",
145
- name: "image",
146
- value: image,
147
- onChange: setImage,
148
- placeholder: "https://example.com/avatar.png",
149
- hint: "Optional — publicly accessible image URL"
150
- }
151
- )
152
- ] }),
153
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
154
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Password" }),
155
- /* @__PURE__ */ jsxRuntime.jsx(
156
- designSystem.Checkbox,
157
- {
158
- name: "generatePassword",
159
- checked: generatePassword,
160
- onCheckedChange: (checked) => {
161
- setGeneratePassword(checked);
162
- if (checked) setPassword("");
163
- },
164
- children: "Generate a random password"
165
- }
166
- ),
167
- !generatePassword && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { style: { width: "100%" }, children: [
168
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Password" }),
169
- /* @__PURE__ */ jsxRuntime.jsx(
170
- designSystem.TextInput,
171
- {
172
- name: "password",
173
- type: "password",
174
- value: password,
175
- onChange: (e) => setPassword(e.target.value),
176
- placeholder: "Leave blank for a passwordless account"
177
- }
178
- )
179
- ] })
180
- ] }),
181
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
182
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Email verification" }),
183
- /* @__PURE__ */ jsxRuntime.jsx(
184
- designSystem.Checkbox,
185
- {
186
- name: "emailVerified",
187
- checked: emailVerified,
188
- onCheckedChange: (checked) => {
189
- setEmailVerified(checked);
190
- if (checked) setSendVerificationEmail(false);
191
- },
192
- children: "Mark email as already verified"
193
- }
194
- ),
195
- !emailVerified && /* @__PURE__ */ jsxRuntime.jsx(
196
- designSystem.Checkbox,
197
- {
198
- name: "sendVerificationEmail",
199
- checked: sendVerificationEmail,
200
- onCheckedChange: (checked) => setSendVerificationEmail(checked),
201
- children: "Send a verification email to the user"
202
- }
203
- ),
204
- emailVerified && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "success600", children: "The user will be able to sign in immediately without verifying their email." }) })
205
- ] })
206
- ] })
207
- }
208
- );
209
- }
210
- function usePluginSettings() {
211
- const { get } = admin.useFetchClient();
212
- return reactQuery.useQuery({
213
- queryKey: ["dash-plugin-settings"],
214
- queryFn: async () => {
215
- const { data } = await get(
216
- "/better-auth-dashboard/settings"
217
- );
218
- return data;
219
- },
220
- staleTime: 5 * 60 * 1e3
221
- });
222
- }
223
- const TwoFaChip = styled__default.default.span`
224
- display: inline-flex;
225
- align-items: center;
226
- gap: 5px;
227
- padding: 3px 10px;
228
- border-radius: 20px;
229
- font-size: 11px;
230
- font-weight: 700;
231
- background: ${(p) => p.$enabled ? "#eafbe7" : "#f0f0ff"};
232
- color: ${(p) => p.$enabled ? "#5cb176" : "#8e8ea9"};
233
- &::before {
234
- content: '';
235
- display: inline-block;
236
- width: 5px;
237
- height: 5px;
238
- border-radius: 50%;
239
- background: ${(p) => p.$enabled ? "#5cb176" : "#b8b8c7"};
240
- }
241
- `;
242
- const BackupCodeGrid = styled__default.default.div`
243
- display: grid;
244
- grid-template-columns: repeat(2, 1fr);
245
- gap: 6px;
246
- `;
247
- const BackupCodeItem = styled__default.default.span`
248
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
249
- font-size: 12px;
250
- font-weight: 600;
251
- padding: 6px 10px;
252
- background: white;
253
- border: 1px solid #eaeaef;
254
- border-radius: 6px;
255
- color: #32324d;
256
- letter-spacing: 0.05em;
257
- text-align: center;
258
- `;
259
- const ReadOnlyCodeInput = styled__default.default.div`
260
- display: flex;
261
- align-items: center;
262
- gap: 8px;
263
- padding: 8px 12px;
264
- background: white;
265
- border: 1px solid #eaeaef;
266
- border-radius: 6px;
267
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
268
- font-size: 11px;
269
- color: #32324d;
270
- word-break: break-all;
271
- overflow-wrap: anywhere;
272
- `;
273
- const SessionCard = styled__default.default.div`
274
- display: flex;
275
- align-items: flex-start;
276
- justify-content: space-between;
277
- gap: 12px;
278
- padding: 10px 14px;
279
- background: white;
280
- border: 1px solid #eaeaef;
281
- border-radius: 8px;
282
- `;
283
- const SessionMeta = styled__default.default.div`
284
- display: flex;
285
- flex-direction: column;
286
- gap: 3px;
287
- flex: 1;
288
- min-width: 0;
289
- `;
290
- const IpChip = styled__default.default.span`
291
- display: inline-block;
292
- background: #f0f0ff;
293
- color: #4945ff;
294
- border-radius: 5px;
295
- padding: 2px 6px;
296
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
297
- font-size: 11px;
298
- font-weight: 600;
299
- `;
300
- const TimestampText = styled__default.default.span`
301
- font-size: 11px;
302
- color: #8e8ea9;
303
- `;
304
- const AgentText = styled__default.default.span`
305
- font-size: 11px;
306
- color: #b8b8c7;
307
- overflow: hidden;
308
- text-overflow: ellipsis;
309
- white-space: nowrap;
310
- display: block;
311
- `;
312
- const STANDARD_FIELDS = /* @__PURE__ */ new Set([
313
- "id",
314
- "name",
315
- "email",
316
- "emailVerified",
317
- "image",
318
- "banned",
319
- "banReason",
320
- "banExpires",
321
- "createdAt",
322
- "updatedAt"
323
- ]);
324
- function UserDetailDrawer({
325
- userId,
326
- banEnabled,
327
- emailVerificationEnabled,
328
- twoFactorEnabled,
329
- onClose
330
- }) {
331
- const qc = reactQuery.useQueryClient();
332
- const { toggleNotification } = admin.useNotification();
333
- const { get, put } = admin.useFetchClient();
334
- const schemaQuery = useModelSchema.useModelSchema("user");
335
- const settingsQuery = usePluginSettings();
336
- const emailCallbackUrl = settingsQuery.data?.email_callback_url ?? null;
337
- const userQuery = reactQuery.useQuery({
338
- queryKey: ["dash-user", userId],
339
- queryFn: async () => {
340
- const result = await index.client.dash.user(
341
- {},
342
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
343
- );
344
- if (result.error)
345
- throw new Error(result.error.message ?? "Failed to load user");
346
- return result.data;
347
- }
348
- });
349
- const orgsQuery = reactQuery.useQuery({
350
- queryKey: ["dash-user-orgs", userId],
351
- queryFn: async () => {
352
- const result = await index.client.dash.userOrganizations(
353
- {},
354
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
355
- );
356
- if (result.error) throw new Error(result.error.message ?? "Failed");
357
- return result.data?.organizations ?? [];
358
- }
359
- });
360
- const sessionsQuery = reactQuery.useQuery({
361
- queryKey: ["dash-user-sessions", userId],
362
- queryFn: async () => {
363
- const { data } = await get(
364
- `/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
365
- );
366
- return data.results ?? [];
367
- }
368
- });
369
- const strapiUserQuery = reactQuery.useQuery({
370
- queryKey: ["dash-strapi-user", userId],
371
- enabled: !!schemaQuery.data,
372
- queryFn: async () => {
373
- const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
374
- const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
375
- const { data } = await get(
376
- `/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
377
- );
378
- return data.results?.[0] ?? null;
379
- }
380
- });
381
- const [activeTab, setActiveTab] = react.useState("profile");
382
- const [editName, setEditName] = react.useState(void 0);
383
- const [editEmail, setEditEmail] = react.useState(void 0);
384
- const [editEmailVerified, setEditEmailVerified] = react.useState(void 0);
385
- const [editImage, setEditImage] = react.useState(void 0);
386
- const [editExtra, setEditExtra] = react.useState({});
387
- const [newPassword, setNewPassword] = react.useState("");
388
- const [banReason, setBanReason] = react.useState("");
389
- const [banExpiresDays, setBanExpiresDays] = react.useState("");
390
- const [confirmRevokeAll, setConfirmRevokeAll] = react.useState(false);
391
- const [confirmRevokeSessionId, setConfirmRevokeSessionId] = react.useState(null);
392
- const [confirmBan, setConfirmBan] = react.useState(false);
393
- const [confirmUnban, setConfirmUnban] = react.useState(false);
394
- const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = react.useState(null);
395
- const [confirmDisable2FA, setConfirmDisable2FA] = react.useState(false);
396
- const [showTotpModal, setShowTotpModal] = react.useState(false);
397
- const [showBackupCodesModal, setShowBackupCodesModal] = react.useState(false);
398
- const [confirmRegenerateBackupCodes, setConfirmRegenerateBackupCodes] = react.useState(false);
399
- const [twoFactorData, setTwoFactorData] = react.useState(null);
400
- const [totpUriData, setTotpUriData] = react.useState(null);
401
- const [backupCodesData, setBackupCodesData] = react.useState(null);
402
- const user = userQuery.data;
403
- const displayName = editName ?? user?.name ?? "";
404
- const displayEmail = editEmail ?? user?.email ?? "";
405
- const displayEmailVerified = editEmailVerified ?? user?.emailVerified ?? false;
406
- const displayImage = editImage ?? user?.image ?? "";
407
- const handleExtraChange = (name, value) => {
408
- setEditExtra((prev) => ({ ...prev, [name]: value }));
409
- };
410
- const extraData = {
411
- ...strapiUserQuery.data ?? {},
412
- ...editExtra
413
- };
414
- const updateMutation = reactQuery.useMutation({
415
- mutationFn: async () => {
416
- const body = {};
417
- if (editName !== void 0) body.name = editName;
418
- if (editEmail !== void 0) body.email = editEmail;
419
- if (editEmailVerified !== void 0)
420
- body.emailVerified = editEmailVerified;
421
- if (editImage !== void 0) body.image = editImage;
422
- const documentId = strapiUserQuery.data?.documentId;
423
- if (!documentId) throw new Error("Could not resolve documentId for user");
424
- const updateUser = await index.client.dash.updateUser(
425
- { userId, ...body },
426
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
427
- );
428
- if (!updateUser.data) throw new Error("Update failed");
429
- if (Object.keys(editExtra).length > 0) {
430
- await put(
431
- `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
432
- editExtra
433
- );
434
- }
435
- },
436
- onSuccess: async () => {
437
- await qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
438
- qc.invalidateQueries({ queryKey: ["dash-user", userId] });
439
- qc.invalidateQueries({ queryKey: ["dash-users"] });
440
- setEditName(void 0);
441
- setEditEmail(void 0);
442
- setEditEmailVerified(void 0);
443
- setEditImage(void 0);
444
- setEditExtra({});
445
- toggleNotification({
446
- type: "success",
447
- message: "User updated successfully"
448
- });
449
- onClose();
450
- },
451
- onError: (err) => {
452
- toggleNotification({
453
- type: "danger",
454
- message: err.message ?? "Update failed"
455
- });
456
- }
457
- });
458
- const revokeSessionMutation = reactQuery.useMutation({
459
- mutationFn: async (sessionId) => {
460
- const result = await index.client.dash.sessions.revoke(
461
- {},
462
- useModelSchema.withContext({ sessionId, userId }, index.getAuthHeaders())
463
- );
464
- if (result.error)
465
- throw new Error(result.error.message ?? "Revoke failed");
466
- },
467
- onSuccess: () => {
468
- setConfirmRevokeSessionId(null);
469
- qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
470
- toggleNotification({ type: "success", message: "Session revoked" });
471
- },
472
- onError: (err) => {
473
- toggleNotification({
474
- type: "danger",
475
- message: err.message ?? "Failed to revoke session"
476
- });
477
- }
478
- });
479
- const passwordMutation = reactQuery.useMutation({
480
- mutationFn: async () => {
481
- const result = await index.client.dash.setPassword(
482
- { password: newPassword },
483
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
484
- );
485
- if (result.error)
486
- throw new Error(result.error.message ?? "Password update failed");
487
- return result.data;
488
- },
489
- onSuccess: () => {
490
- setNewPassword("");
491
- toggleNotification({
492
- type: "success",
493
- message: "Password updated successfully"
494
- });
495
- },
496
- onError: (err) => {
497
- toggleNotification({
498
- type: "danger",
499
- message: err.message ?? "Password update failed"
500
- });
501
- }
502
- });
503
- const banMutation = reactQuery.useMutation({
504
- mutationFn: async () => {
505
- const body = {};
506
- if (banReason) body.banReason = banReason;
507
- if (banExpiresDays) {
508
- const days = parseInt(banExpiresDays, 10);
509
- if (!Number.isNaN(days)) body.banExpires = Date.now() + days * 864e5;
510
- }
511
- const result = await index.client.dash.banUser(
512
- body,
513
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
514
- );
515
- if (result.error) throw new Error(result.error.message ?? "Ban failed");
516
- return result.data;
517
- },
518
- onSuccess: () => {
519
- setConfirmBan(false);
520
- qc.invalidateQueries({ queryKey: ["dash-user", userId] });
521
- qc.invalidateQueries({ queryKey: ["dash-users"] });
522
- setBanReason("");
523
- setBanExpiresDays("");
524
- toggleNotification({ type: "success", message: "User has been banned" });
525
- },
526
- onError: (err) => {
527
- toggleNotification({
528
- type: "danger",
529
- message: err.message ?? "Ban failed"
530
- });
531
- }
532
- });
533
- const unbanMutation = reactQuery.useMutation({
534
- mutationFn: async () => {
535
- const result = await index.client.dash.unbanUser(
536
- {},
537
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
538
- );
539
- if (result.error) throw new Error(result.error.message ?? "Unban failed");
540
- return result.data;
541
- },
542
- onSuccess: () => {
543
- setConfirmUnban(false);
544
- qc.invalidateQueries({ queryKey: ["dash-user", userId] });
545
- qc.invalidateQueries({ queryKey: ["dash-users"] });
546
- toggleNotification({ type: "success", message: "Ban has been lifted" });
547
- },
548
- onError: (err) => {
549
- toggleNotification({
550
- type: "danger",
551
- message: err.message ?? "Unban failed"
552
- });
553
- }
554
- });
555
- const unlinkAccountMutation = reactQuery.useMutation({
556
- mutationFn: async (providerId) => {
557
- const result = await index.client.dash.unlinkAccount(
558
- { providerId },
559
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
560
- );
561
- if (result.error)
562
- throw new Error(result.error.message ?? "Failed to unlink account");
563
- },
564
- onSuccess: () => {
565
- setConfirmUnlinkAccountId(null);
566
- qc.invalidateQueries({ queryKey: ["dash-user", userId] });
567
- toggleNotification({ type: "success", message: "Account unlinked" });
568
- },
569
- onError: (err) => {
570
- toggleNotification({
571
- type: "danger",
572
- message: err.message ?? "Failed to unlink account"
573
- });
574
- }
575
- });
576
- const revokeAllMutation = reactQuery.useMutation({
577
- mutationFn: async () => {
578
- const result = await index.client.dash.sessions.revokeAll(
579
- { userId },
580
- useModelSchema.withContext({}, index.getAuthHeaders())
581
- );
582
- if (result.error)
583
- throw new Error(result.error.message ?? "Revoke failed");
584
- return result.data;
585
- },
586
- onSuccess: () => {
587
- setConfirmRevokeAll(false);
588
- toggleNotification({ type: "success", message: "All sessions revoked" });
589
- },
590
- onError: (err) => {
591
- toggleNotification({
592
- type: "danger",
593
- message: err.message ?? "Failed to revoke sessions"
594
- });
595
- }
596
- });
597
- const sendVerificationMutation = reactQuery.useMutation({
598
- mutationFn: async () => {
599
- if (!emailCallbackUrl) {
600
- throw new Error(
601
- "Set `email_callback_url` in the better-auth-dashboard plugin config to enable this action."
602
- );
603
- }
604
- const result = await index.client.dash.sendVerificationEmail(
605
- { callbackUrl: emailCallbackUrl },
606
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
607
- );
608
- if (result.error) throw new Error(result.error.message ?? "Failed");
609
- return result.data;
610
- },
611
- onSuccess: () => {
612
- toggleNotification({
613
- type: "success",
614
- message: "Verification email sent"
615
- });
616
- },
617
- onError: (err) => {
618
- toggleNotification({
619
- type: "danger",
620
- message: err.message ?? "Failed to send email"
621
- });
622
- }
623
- });
624
- const sendResetPasswordMutation = reactQuery.useMutation({
625
- mutationFn: async () => {
626
- if (!emailCallbackUrl) {
627
- throw new Error(
628
- "Set `email_callback_url` in the better-auth-dashboard plugin config to enable this action."
629
- );
630
- }
631
- const result = await index.client.dash.sendResetPasswordEmail(
632
- { callbackUrl: emailCallbackUrl },
633
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
634
- );
635
- if (result.error) throw new Error(result.error.message ?? "Failed");
636
- return result.data;
637
- },
638
- onSuccess: () => {
639
- toggleNotification({
640
- type: "success",
641
- message: "Password reset email sent"
642
- });
643
- },
644
- onError: (err) => {
645
- toggleNotification({
646
- type: "danger",
647
- message: err.message ?? "Failed to send email"
648
- });
649
- }
650
- });
651
- const enableTwoFactorMutation = reactQuery.useMutation({
652
- mutationFn: async () => {
653
- const result = await index.client.dash.enableTwoFactor(
654
- {},
655
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
656
- );
657
- if (result.error)
658
- throw new Error(result.error.message ?? "Failed to enable 2FA");
659
- return result.data;
660
- },
661
- onSuccess: (data) => {
662
- if (data) setTwoFactorData(data);
663
- qc.invalidateQueries({ queryKey: ["dash-user", userId] });
664
- toggleNotification({
665
- type: "success",
666
- message: "Two-factor authentication enabled"
667
- });
668
- },
669
- onError: (err) => {
670
- toggleNotification({
671
- type: "danger",
672
- message: err.message ?? "Failed to enable 2FA"
673
- });
674
- }
675
- });
676
- const disableTwoFactorMutation = reactQuery.useMutation({
677
- mutationFn: async () => {
678
- const result = await index.client.dash.disableTwoFactor(
679
- {},
680
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
681
- );
682
- if (result.error)
683
- throw new Error(result.error.message ?? "Failed to disable 2FA");
684
- return result.data;
685
- },
686
- onSuccess: () => {
687
- setConfirmDisable2FA(false);
688
- setTwoFactorData(null);
689
- setTotpUriData(null);
690
- setBackupCodesData(null);
691
- qc.invalidateQueries({ queryKey: ["dash-user", userId] });
692
- toggleNotification({
693
- type: "success",
694
- message: "Two-factor authentication disabled"
695
- });
696
- },
697
- onError: (err) => {
698
- toggleNotification({
699
- type: "danger",
700
- message: err.message ?? "Failed to disable 2FA"
701
- });
702
- }
703
- });
704
- const viewTotpUriMutation = reactQuery.useMutation({
705
- mutationFn: async () => {
706
- const result = await index.client.dash.viewTwoFactorTotpUri(
707
- {},
708
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
709
- );
710
- if (result.error)
711
- throw new Error(result.error.message ?? "Failed to load TOTP URI");
712
- return result.data;
713
- },
714
- onSuccess: (data) => {
715
- if (data) {
716
- setTotpUriData(data);
717
- setShowTotpModal(true);
718
- }
719
- },
720
- onError: (err) => {
721
- toggleNotification({
722
- type: "danger",
723
- message: err.message ?? "Failed to load TOTP URI"
724
- });
725
- }
726
- });
727
- const viewBackupCodesMutation = reactQuery.useMutation({
728
- mutationFn: async () => {
729
- const result = await index.client.dash.viewBackupCodes(
730
- {},
731
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
732
- );
733
- if (result.error)
734
- throw new Error(result.error.message ?? "Failed to load backup codes");
735
- return result.data;
736
- },
737
- onSuccess: (data) => {
738
- if (data) {
739
- setBackupCodesData(data.backupCodes);
740
- setShowBackupCodesModal(true);
741
- }
742
- },
743
- onError: (err) => {
744
- toggleNotification({
745
- type: "danger",
746
- message: err.message ?? "Failed to load backup codes"
747
- });
748
- }
749
- });
750
- const generateBackupCodesMutation = reactQuery.useMutation({
751
- mutationFn: async () => {
752
- const result = await index.client.dash.generateBackupCodes(
753
- {},
754
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
755
- );
756
- if (result.error)
757
- throw new Error(
758
- result.error.message ?? "Failed to generate backup codes"
759
- );
760
- return result.data;
761
- },
762
- onSuccess: (data) => {
763
- setConfirmRegenerateBackupCodes(false);
764
- if (data) {
765
- setBackupCodesData(data.backupCodes);
766
- setShowBackupCodesModal(true);
767
- }
768
- toggleNotification({
769
- type: "success",
770
- message: "New backup codes generated"
771
- });
772
- },
773
- onError: (err) => {
774
- toggleNotification({
775
- type: "danger",
776
- message: err.message ?? "Failed to generate backup codes"
777
- });
778
- }
779
- });
780
- const hasEdits = editName !== void 0 || editEmail !== void 0 || editEmailVerified !== void 0 || editImage !== void 0 || Object.keys(editExtra).length > 0;
781
- const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
782
- const banExpiryPreview = banExpiresDays && !Number.isNaN(parseInt(banExpiresDays, 10)) ? new Date(
783
- Date.now() + parseInt(banExpiresDays, 10) * 864e5
784
- ).toLocaleDateString(void 0, {
785
- weekday: "long",
786
- year: "numeric",
787
- month: "long",
788
- day: "numeric"
789
- }) : null;
790
- const drawerTitle = /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
791
- /* @__PURE__ */ jsxRuntime.jsx(Avatar.Avatar, { name: user?.name ?? "?", src: user?.image, size: 30 }),
792
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
793
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", tag: "span", children: user?.name ?? "User" }),
794
- user?.email && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: user.email }) })
795
- ] }),
796
- user?.banned && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "danger100", textColor: "danger600", children: "Banned" })
797
- ] });
798
- const profileFooter = activeTab === "profile" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
799
- /* @__PURE__ */ jsxRuntime.jsx(
800
- designSystem.Button,
801
- {
802
- variant: "tertiary",
803
- onClick: () => {
804
- setEditName(void 0);
805
- setEditEmail(void 0);
806
- setEditEmailVerified(void 0);
807
- setEditImage(void 0);
808
- setEditExtra({});
809
- },
810
- children: "Discard"
811
- }
812
- ),
813
- /* @__PURE__ */ jsxRuntime.jsx(
814
- designSystem.Button,
815
- {
816
- disabled: !hasEdits,
817
- loading: updateMutation.isLoading,
818
- onClick: () => updateMutation.mutate(),
819
- "data-testid": "save-user-btn",
820
- children: "Save changes"
821
- }
822
- )
823
- ] }) : void 0;
824
- return /* @__PURE__ */ jsxRuntime.jsxs(
825
- useModelSchema.Drawer,
826
- {
827
- title: drawerTitle,
828
- footer: profileFooter,
829
- onClose,
830
- "data-testid": "user-detail-drawer",
831
- children: [
832
- userQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral500", children: "Loading user…" }) }) : userQuery.isError ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Alert, { closeLabel: "Close", title: "Failed to load user", variant: "danger", children: userQuery.error instanceof Error ? userQuery.error.message : "An error occurred" }) : /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Root, { defaultValue: "profile", onValueChange: setActiveTab, children: [
833
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.List, { children: [
834
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "profile", children: "Profile" }),
835
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "security", children: "Security" }),
836
- banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "ban", children: "Ban" }),
837
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "sessions", children: "Sessions" }),
838
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "organizations", children: "Organizations" })
839
- ] }),
840
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "profile", children: /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.EditLayout, { children: [
841
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, children: [
842
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
843
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Personal information" }),
844
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { style: { width: "100%" }, children: [
845
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Full name" }),
846
- /* @__PURE__ */ jsxRuntime.jsx(
847
- designSystem.TextInput,
848
- {
849
- value: displayName,
850
- onChange: (e) => setEditName(e.target.value),
851
- "data-testid": "user-name-input"
852
- }
853
- )
854
- ] }),
855
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { style: { width: "100%" }, children: [
856
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Email address" }),
857
- /* @__PURE__ */ jsxRuntime.jsx(
858
- designSystem.TextInput,
859
- {
860
- type: "email",
861
- value: displayEmail,
862
- onChange: (e) => setEditEmail(e.target.value)
863
- }
864
- )
865
- ] }),
866
- /* @__PURE__ */ jsxRuntime.jsx(
867
- designSystem.Checkbox,
868
- {
869
- checked: displayEmailVerified,
870
- onCheckedChange: (checked) => setEditEmailVerified(checked),
871
- children: "Mark email as verified"
872
- }
873
- ),
874
- /* @__PURE__ */ jsxRuntime.jsx(
875
- useModelSchema.MediaPickerField,
876
- {
877
- label: "Avatar URL",
878
- name: "image",
879
- value: displayImage,
880
- onChange: setEditImage,
881
- placeholder: "https://example.com/avatar.png",
882
- hint: "Optional — publicly accessible image URL"
883
- }
884
- )
885
- ] }),
886
- /* @__PURE__ */ jsxRuntime.jsx(
887
- useModelSchema.CustomFieldsSection,
888
- {
889
- fields: customFields,
890
- data: extraData,
891
- onChange: handleExtraChange
892
- }
893
- )
894
- ] }),
895
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.EditSidebar, { children: [
896
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
897
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Details" }),
898
- /* @__PURE__ */ jsxRuntime.jsxs(
899
- "div",
900
- {
901
- style: {
902
- display: "grid",
903
- gridTemplateColumns: "1fr 1fr",
904
- gap: 12
905
- },
906
- children: [
907
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.MetaItem, { style: { gridColumn: "1 / -1" }, children: [
908
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.MetaKey, { children: "User ID" }),
909
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.MonoChip, { children: user?.id })
910
- ] }),
911
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.MetaItem, { children: [
912
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.MetaKey, { children: "Created" }),
913
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.MetaVal, { children: user?.createdAt ? new Date(user.createdAt).toLocaleString() : "—" })
914
- ] }),
915
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.MetaItem, { children: [
916
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.MetaKey, { children: "Last updated" }),
917
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.MetaVal, { children: user?.updatedAt ? new Date(user.updatedAt).toLocaleString() : "—" })
918
- ] })
919
- ]
920
- }
921
- )
922
- ] }),
923
- emailVerificationEnabled && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
924
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { style: { marginBottom: 10 }, children: "Email actions" }),
925
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 2, children: [
926
- /* @__PURE__ */ jsxRuntime.jsx(
927
- designSystem.Button,
928
- {
929
- variant: "secondary",
930
- size: "S",
931
- loading: sendVerificationMutation.isLoading,
932
- disabled: !emailCallbackUrl || user?.emailVerified,
933
- onClick: () => sendVerificationMutation.mutate(),
934
- style: { width: "100%" },
935
- children: "Send verification email"
936
- }
937
- ),
938
- /* @__PURE__ */ jsxRuntime.jsx(
939
- designSystem.Button,
940
- {
941
- variant: "secondary",
942
- size: "S",
943
- loading: sendResetPasswordMutation.isLoading,
944
- disabled: !emailCallbackUrl,
945
- onClick: () => sendResetPasswordMutation.mutate(),
946
- style: { width: "100%" },
947
- children: "Send password reset"
948
- }
949
- ),
950
- !settingsQuery.isLoading && !emailCallbackUrl && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Set `email_callback_url` in the better-auth-dashboard plugin config to enable these actions." })
951
- ] })
952
- ] }),
953
- /* @__PURE__ */ jsxRuntime.jsx(
954
- useModelSchema.EditViewSidePanels,
955
- {
956
- model: "plugin::better-auth.user",
957
- documentId: strapiUserQuery.data?.documentId,
958
- document: strapiUserQuery.data ?? void 0
959
- }
960
- )
961
- ] })
962
- ] }) }),
963
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "security", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
964
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
965
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Password" }),
966
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { hint: "The user will be able to sign in with this new password", children: [
967
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "New password" }),
968
- /* @__PURE__ */ jsxRuntime.jsx(
969
- designSystem.TextInput,
970
- {
971
- type: "password",
972
- value: newPassword,
973
- onChange: (e) => setNewPassword(e.target.value),
974
- placeholder: "Enter a strong password…"
975
- }
976
- ),
977
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
978
- ] }),
979
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
980
- designSystem.Button,
981
- {
982
- disabled: !newPassword,
983
- loading: passwordMutation.isLoading,
984
- onClick: () => passwordMutation.mutate(),
985
- children: "Set password"
986
- }
987
- ) })
988
- ] }),
989
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
990
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Linked accounts" }),
991
- user?.account && user.account.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, children: user.account.map((acc) => /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.AccountRow, { children: [
992
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.ProviderBadge, { children: acc.providerId }),
993
- /* @__PURE__ */ jsxRuntime.jsx(
994
- designSystem.Button,
995
- {
996
- variant: "danger-light",
997
- size: "S",
998
- onClick: () => setConfirmUnlinkAccountId(acc.providerId),
999
- children: "Unlink"
1000
- }
1001
- )
1002
- ] }, acc.id)) }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "No linked OAuth accounts." })
1003
- ] }),
1004
- twoFactorEnabled && /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
1005
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
1006
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { style: { margin: 0 }, children: "Two-factor authentication" }),
1007
- /* @__PURE__ */ jsxRuntime.jsx(
1008
- TwoFaChip,
1009
- {
1010
- $enabled: !!user?.twoFactorEnabled,
1011
- children: user?.twoFactorEnabled ? "Enabled" : "Not enabled"
1012
- }
1013
- )
1014
- ] }),
1015
- user?.twoFactorEnabled ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 2, children: [
1016
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.AccountRow, { children: [
1017
- /* @__PURE__ */ jsxRuntime.jsxs(
1018
- designSystem.Flex,
1019
- {
1020
- direction: "column",
1021
- gap: 1,
1022
- alignItems: "flex-start",
1023
- children: [
1024
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "TOTP authenticator" }),
1025
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "View the URI and secret for this user's authenticator app." })
1026
- ]
1027
- }
1028
- ),
1029
- /* @__PURE__ */ jsxRuntime.jsx(
1030
- designSystem.Button,
1031
- {
1032
- variant: "secondary",
1033
- size: "S",
1034
- loading: viewTotpUriMutation.isLoading,
1035
- onClick: () => viewTotpUriMutation.mutate(),
1036
- children: totpUriData ? "Refresh" : "View"
1037
- }
1038
- )
1039
- ] }),
1040
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.AccountRow, { children: [
1041
- /* @__PURE__ */ jsxRuntime.jsxs(
1042
- designSystem.Flex,
1043
- {
1044
- direction: "column",
1045
- gap: 1,
1046
- alignItems: "flex-start",
1047
- children: [
1048
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Backup codes" }),
1049
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "View or regenerate the user's recovery codes." })
1050
- ]
1051
- }
1052
- ),
1053
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
1054
- /* @__PURE__ */ jsxRuntime.jsx(
1055
- designSystem.Button,
1056
- {
1057
- variant: "secondary",
1058
- size: "S",
1059
- loading: viewBackupCodesMutation.isLoading,
1060
- onClick: () => viewBackupCodesMutation.mutate(),
1061
- children: backupCodesData ? "Refresh" : "View"
1062
- }
1063
- ),
1064
- /* @__PURE__ */ jsxRuntime.jsx(
1065
- designSystem.Button,
1066
- {
1067
- variant: "secondary",
1068
- size: "S",
1069
- onClick: () => setConfirmRegenerateBackupCodes(true),
1070
- children: "Regenerate"
1071
- }
1072
- )
1073
- ] })
1074
- ] }),
1075
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.DangerRow, { children: [
1076
- /* @__PURE__ */ jsxRuntime.jsxs(
1077
- designSystem.Flex,
1078
- {
1079
- direction: "column",
1080
- gap: 1,
1081
- alignItems: "flex-start",
1082
- children: [
1083
- /* @__PURE__ */ jsxRuntime.jsx(
1084
- designSystem.Typography,
1085
- {
1086
- variant: "omega",
1087
- fontWeight: "semiBold",
1088
- textColor: "danger600",
1089
- children: "Disable two-factor authentication"
1090
- }
1091
- ),
1092
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger500", children: "The user will no longer need a second factor to sign in." })
1093
- ]
1094
- }
1095
- ),
1096
- /* @__PURE__ */ jsxRuntime.jsx(
1097
- designSystem.Button,
1098
- {
1099
- variant: "danger-light",
1100
- size: "S",
1101
- onClick: () => setConfirmDisable2FA(true),
1102
- children: "Disable 2FA"
1103
- }
1104
- )
1105
- ] })
1106
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 2, children: [
1107
- twoFactorData && /* @__PURE__ */ jsxRuntime.jsxs(
1108
- designSystem.Flex,
1109
- {
1110
- direction: "column",
1111
- gap: 3,
1112
- style: { padding: "2px 4px" },
1113
- children: [
1114
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Scan the TOTP URI with an authenticator app, or enter the secret key manually." }),
1115
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
1116
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "TOTP URI" }),
1117
- /* @__PURE__ */ jsxRuntime.jsx(ReadOnlyCodeInput, { children: twoFactorData.totpURI })
1118
- ] }),
1119
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
1120
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Secret key" }),
1121
- /* @__PURE__ */ jsxRuntime.jsx(ReadOnlyCodeInput, { children: twoFactorData.secret })
1122
- ] }),
1123
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 2, children: [
1124
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Backup codes" }),
1125
- /* @__PURE__ */ jsxRuntime.jsx(BackupCodeGrid, { children: twoFactorData.backupCodes.map((code) => /* @__PURE__ */ jsxRuntime.jsx(BackupCodeItem, { children: code }, code)) })
1126
- ] })
1127
- ]
1128
- }
1129
- ),
1130
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.AccountRow, { children: [
1131
- /* @__PURE__ */ jsxRuntime.jsxs(
1132
- designSystem.Flex,
1133
- {
1134
- direction: "column",
1135
- gap: 1,
1136
- alignItems: "flex-start",
1137
- children: [
1138
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Enable two-factor authentication" }),
1139
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Add an extra layer of security to this account." })
1140
- ]
1141
- }
1142
- ),
1143
- /* @__PURE__ */ jsxRuntime.jsx(
1144
- designSystem.Button,
1145
- {
1146
- size: "S",
1147
- loading: enableTwoFactorMutation.isLoading,
1148
- onClick: () => enableTwoFactorMutation.mutate(),
1149
- children: "Enable 2FA"
1150
- }
1151
- )
1152
- ] })
1153
- ] })
1154
- ] })
1155
- ] }) }),
1156
- banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
1157
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Ban status" }),
1158
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.WarnCard, { children: [
1159
- /* @__PURE__ */ jsxRuntime.jsx(
1160
- designSystem.Typography,
1161
- {
1162
- variant: "omega",
1163
- fontWeight: "semiBold",
1164
- textColor: "danger600",
1165
- children: "This user is currently banned"
1166
- }
1167
- ),
1168
- user.banReason && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
1169
- "Reason: ",
1170
- user.banReason
1171
- ] }),
1172
- user.banExpires ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
1173
- "Expires:",
1174
- " ",
1175
- new Date(user.banExpires).toLocaleDateString(
1176
- void 0,
1177
- {
1178
- weekday: "long",
1179
- year: "numeric",
1180
- month: "long",
1181
- day: "numeric"
1182
- }
1183
- )
1184
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
1185
- ] }),
1186
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
1187
- designSystem.Button,
1188
- {
1189
- variant: "secondary",
1190
- onClick: () => setConfirmUnban(true),
1191
- children: "Lift ban"
1192
- }
1193
- ) })
1194
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
1195
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Apply ban" }),
1196
- /* @__PURE__ */ jsxRuntime.jsxs(
1197
- designSystem.Field.Root,
1198
- {
1199
- hint: "Optional — will be shown to the user",
1200
- style: { width: "100%" },
1201
- children: [
1202
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
1203
- /* @__PURE__ */ jsxRuntime.jsx(
1204
- designSystem.TextInput,
1205
- {
1206
- value: banReason,
1207
- onChange: (e) => setBanReason(e.target.value),
1208
- placeholder: "e.g. Violated terms of service"
1209
- }
1210
- ),
1211
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
1212
- ]
1213
- }
1214
- ),
1215
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
1216
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(
1217
- designSystem.Field.Root,
1218
- {
1219
- hint: "Leave empty for a permanent ban",
1220
- style: { width: "100%" },
1221
- children: [
1222
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Duration (days)" }),
1223
- /* @__PURE__ */ jsxRuntime.jsx(
1224
- designSystem.TextInput,
1225
- {
1226
- type: "number",
1227
- value: banExpiresDays,
1228
- onChange: (e) => setBanExpiresDays(e.target.value),
1229
- placeholder: "e.g. 30"
1230
- }
1231
- ),
1232
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
1233
- ]
1234
- }
1235
- ) }),
1236
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsx(
1237
- designSystem.Flex,
1238
- {
1239
- direction: "column",
1240
- justifyContent: "flex-end",
1241
- style: { height: "100%", paddingBottom: 4 },
1242
- children: /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
1243
- }
1244
- ) })
1245
- ] }),
1246
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
1247
- designSystem.Button,
1248
- {
1249
- variant: "danger",
1250
- onClick: () => setConfirmBan(true),
1251
- children: "Ban user"
1252
- }
1253
- ) })
1254
- ] }) }) }),
1255
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
1256
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
1257
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Session management" }),
1258
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.AccountRow, { children: [
1259
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
1260
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
1261
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
1262
- ] }),
1263
- /* @__PURE__ */ jsxRuntime.jsx(
1264
- designSystem.Button,
1265
- {
1266
- variant: "danger-light",
1267
- size: "S",
1268
- onClick: () => setConfirmRevokeAll(true),
1269
- style: { flexShrink: 0 },
1270
- children: "Revoke all"
1271
- }
1272
- )
1273
- ] })
1274
- ] }),
1275
- /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
1276
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Active sessions" }),
1277
- sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading sessions…" }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "No active sessions." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { children: [
1278
- /* @__PURE__ */ jsxRuntime.jsxs(SessionMeta, { children: [
1279
- /* @__PURE__ */ jsxRuntime.jsxs(
1280
- designSystem.Flex,
1281
- {
1282
- gap: 2,
1283
- alignItems: "center",
1284
- style: { flexWrap: "wrap" },
1285
- children: [
1286
- session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
1287
- /* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
1288
- "Created",
1289
- " ",
1290
- new Date(session.createdAt).toLocaleString(),
1291
- " · Expires",
1292
- " ",
1293
- new Date(session.expiresAt).toLocaleString()
1294
- ] })
1295
- ]
1296
- }
1297
- ),
1298
- session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
1299
- ] }),
1300
- /* @__PURE__ */ jsxRuntime.jsx(
1301
- designSystem.IconButton,
1302
- {
1303
- label: "Revoke session",
1304
- onClick: () => setConfirmRevokeSessionId(String(session.id)),
1305
- style: { flexShrink: 0 },
1306
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
1307
- }
1308
- )
1309
- ] }, session.documentId)) })
1310
- ] })
1311
- ] }) }),
1312
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(useModelSchema.FormSection, { children: [
1313
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.SectionLabel, { children: "Memberships" }),
1314
- orgsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral500", children: "Loading…" }) : (orgsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Not a member of any organizations." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, children: (orgsQuery.data ?? []).map(
1315
- (org) => org ? /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.AccountRow, { children: /* @__PURE__ */ jsxRuntime.jsxs(
1316
- designSystem.Flex,
1317
- {
1318
- direction: "column",
1319
- gap: 1,
1320
- alignItems: "flex-start",
1321
- children: [
1322
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: org.name }),
1323
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
1324
- /* @__PURE__ */ jsxRuntime.jsx(useModelSchema.RoleBadge, { $role: org.role, children: org.role }),
1325
- org.teams.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
1326
- org.teams.length,
1327
- " team",
1328
- org.teams.length !== 1 ? "s" : ""
1329
- ] })
1330
- ] })
1331
- ]
1332
- }
1333
- ) }, org.id) : null
1334
- ) })
1335
- ] }) }) })
1336
- ] }),
1337
- confirmRevokeAll && /* @__PURE__ */ jsxRuntime.jsx(
1338
- useModelSchema.ConfirmDialog,
1339
- {
1340
- title: "Revoke all sessions",
1341
- message: "Are you sure you want to revoke all sessions for this user? They will be signed out on all devices.",
1342
- confirmLabel: "Revoke all",
1343
- loading: revokeAllMutation.isLoading,
1344
- onConfirm: () => revokeAllMutation.mutate(),
1345
- onCancel: () => setConfirmRevokeAll(false)
1346
- }
1347
- ),
1348
- confirmRevokeSessionId && /* @__PURE__ */ jsxRuntime.jsx(
1349
- useModelSchema.ConfirmDialog,
1350
- {
1351
- title: "Revoke session",
1352
- message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
1353
- confirmLabel: "Revoke",
1354
- loading: revokeSessionMutation.isLoading,
1355
- onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
1356
- onCancel: () => setConfirmRevokeSessionId(null)
1357
- }
1358
- ),
1359
- confirmBan && /* @__PURE__ */ jsxRuntime.jsx(
1360
- useModelSchema.ConfirmDialog,
1361
- {
1362
- title: "Ban user",
1363
- message: banReason ? `Ban this user for the following reason: "${banReason}"? They will be prevented from signing in.` : "Are you sure you want to ban this user? They will be prevented from signing in.",
1364
- confirmLabel: "Ban user",
1365
- variant: "danger",
1366
- loading: banMutation.isLoading,
1367
- onConfirm: () => banMutation.mutate(),
1368
- onCancel: () => setConfirmBan(false)
1369
- }
1370
- ),
1371
- confirmUnban && /* @__PURE__ */ jsxRuntime.jsx(
1372
- useModelSchema.ConfirmDialog,
1373
- {
1374
- title: "Lift ban",
1375
- message: "Are you sure you want to lift the ban on this user? They will be able to sign in again.",
1376
- confirmLabel: "Lift ban",
1377
- variant: "default",
1378
- loading: unbanMutation.isLoading,
1379
- onConfirm: () => unbanMutation.mutate(),
1380
- onCancel: () => setConfirmUnban(false)
1381
- }
1382
- ),
1383
- confirmUnlinkAccountId && /* @__PURE__ */ jsxRuntime.jsx(
1384
- useModelSchema.ConfirmDialog,
1385
- {
1386
- title: "Unlink account",
1387
- message: `Are you sure you want to unlink the "${confirmUnlinkAccountId}" provider from this user?`,
1388
- confirmLabel: "Unlink",
1389
- loading: unlinkAccountMutation.isLoading,
1390
- onConfirm: () => unlinkAccountMutation.mutate(confirmUnlinkAccountId),
1391
- onCancel: () => setConfirmUnlinkAccountId(null)
1392
- }
1393
- ),
1394
- confirmDisable2FA && /* @__PURE__ */ jsxRuntime.jsx(
1395
- useModelSchema.ConfirmDialog,
1396
- {
1397
- title: "Disable two-factor authentication",
1398
- message: "Are you sure you want to disable 2FA for this user? They will no longer need a second factor to sign in.",
1399
- confirmLabel: "Disable 2FA",
1400
- loading: disableTwoFactorMutation.isLoading,
1401
- onConfirm: () => disableTwoFactorMutation.mutate(),
1402
- onCancel: () => setConfirmDisable2FA(false)
1403
- }
1404
- ),
1405
- confirmRegenerateBackupCodes && /* @__PURE__ */ jsxRuntime.jsx(
1406
- useModelSchema.ConfirmDialog,
1407
- {
1408
- title: "Regenerate backup codes",
1409
- message: "This will invalidate all existing backup codes. New codes will be shown once after generation. Are you sure?",
1410
- confirmLabel: "Regenerate",
1411
- loading: generateBackupCodesMutation.isLoading,
1412
- onConfirm: () => generateBackupCodesMutation.mutate(),
1413
- onCancel: () => setConfirmRegenerateBackupCodes(false)
1414
- }
1415
- ),
1416
- showTotpModal && totpUriData && /* @__PURE__ */ jsxRuntime.jsx(
1417
- designSystem.Modal.Root,
1418
- {
1419
- defaultOpen: true,
1420
- onOpenChange: (open) => !open && setShowTotpModal(false),
1421
- children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Content, { children: [
1422
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", tag: "h2", children: "TOTP authenticator" }) }),
1423
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
1424
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral600", children: "Share these details with the user to set up their authenticator app." }),
1425
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
1426
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "TOTP URI" }),
1427
- /* @__PURE__ */ jsxRuntime.jsx(ReadOnlyCodeInput, { children: totpUriData.totpURI })
1428
- ] }),
1429
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
1430
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Secret key" }),
1431
- /* @__PURE__ */ jsxRuntime.jsx(ReadOnlyCodeInput, { children: totpUriData.secret })
1432
- ] })
1433
- ] }) }),
1434
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Footer, { children: /* @__PURE__ */ jsxRuntime.jsx(
1435
- designSystem.Button,
1436
- {
1437
- variant: "tertiary",
1438
- onClick: () => setShowTotpModal(false),
1439
- children: "Close"
1440
- }
1441
- ) })
1442
- ] })
1443
- }
1444
- ),
1445
- showBackupCodesModal && backupCodesData && /* @__PURE__ */ jsxRuntime.jsx(
1446
- designSystem.Modal.Root,
1447
- {
1448
- defaultOpen: true,
1449
- onOpenChange: (open) => !open && setShowBackupCodesModal(false),
1450
- children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Content, { children: [
1451
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", tag: "h2", children: "Backup codes" }) }),
1452
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
1453
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral600", children: "Each code can only be used once. Store them somewhere safe." }),
1454
- /* @__PURE__ */ jsxRuntime.jsx(BackupCodeGrid, { children: backupCodesData.map((code) => /* @__PURE__ */ jsxRuntime.jsx(BackupCodeItem, { children: code }, code)) })
1455
- ] }) }),
1456
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Footer, { children: /* @__PURE__ */ jsxRuntime.jsx(
1457
- designSystem.Button,
1458
- {
1459
- variant: "tertiary",
1460
- onClick: () => setShowBackupCodesModal(false),
1461
- children: "Close"
1462
- }
1463
- ) })
1464
- ] })
1465
- }
1466
- )
1467
- ]
1468
- }
1469
- );
1470
- }
1471
- const PAGE_SIZE = 25;
1472
- const fadeUp = styled.keyframes`
1473
- from { opacity: 0; transform: translateY(6px); }
1474
- to { opacity: 1; transform: translateY(0); }
1475
- `;
1476
- const Wrap = styled__default.default.div`
1477
- padding: 28px 32px;
1478
- background: #f6f6f9;
1479
- min-height: 100%;
1480
- display: flex;
1481
- flex-direction: column;
1482
- gap: 20px;
1483
- `;
1484
- const PageHeader = styled__default.default.div`
1485
- display: flex;
1486
- justify-content: space-between;
1487
- align-items: flex-start;
1488
- `;
1489
- const TitleBlock = styled__default.default.div`
1490
- display: flex;
1491
- flex-direction: column;
1492
- gap: 4px;
1493
- `;
1494
- const PageTitle = styled__default.default.h1`
1495
- margin: 0;
1496
- font-size: 22px;
1497
- font-weight: 800;
1498
- color: #32324d;
1499
- letter-spacing: -0.03em;
1500
- `;
1501
- const PageSubtitle = styled__default.default.p`
1502
- margin: 0;
1503
- font-size: 12px;
1504
- color: #8e8ea9;
1505
- `;
1506
- const TableCard = styled__default.default.div`
1507
- background: #ffffff;
1508
- border: 1px solid #eaeaef;
1509
- border-radius: 10px;
1510
- overflow: hidden;
1511
- `;
1512
- const Table = styled__default.default.table`
1513
- width: 100%;
1514
- border-collapse: collapse;
1515
- `;
1516
- const TH = styled__default.default.th`
1517
- text-align: left;
1518
- padding: 10px 14px;
1519
- font-size: 10px;
1520
- font-weight: 700;
1521
- color: #8e8ea9;
1522
- text-transform: uppercase;
1523
- letter-spacing: 0.08em;
1524
- border-bottom: 1px solid #eaeaef;
1525
- background: #fafafa;
1526
- white-space: nowrap;
1527
- &:first-child { padding-left: 20px; }
1528
- &:last-child { padding-right: 20px; }
1529
- `;
1530
- const THCheck = styled__default.default(TH)`
1531
- width: 44px;
1532
- `;
1533
- const THActions = styled__default.default(TH)`
1534
- width: 80px;
1535
- `;
1536
- const TD = styled__default.default.td`
1537
- padding: 11px 14px;
1538
- font-size: 12px;
1539
- color: #32324d;
1540
- border-bottom: 1px solid #f5f5f9;
1541
- vertical-align: middle;
1542
- &:first-child { padding-left: 20px; }
1543
- &:last-child { padding-right: 20px; }
1544
- `;
1545
- const TDCheck = styled__default.default(TD)`
1546
- width: 44px;
1547
- `;
1548
- const TDActions = styled__default.default(TD)`
1549
- width: 80px;
1550
- `;
1551
- const TR = styled__default.default.tr`
1552
- animation: ${fadeUp} 280ms ease both;
1553
- animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
1554
- background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
1555
- transition: background 120ms ease;
1556
- &:hover td { background: ${(p) => p.$selected ? "#e8e8ff" : "#fafafe"}; }
1557
- &:last-child td { border-bottom: none; }
1558
- `;
1559
- const StatusChip = styled__default.default.span`
1560
- display: inline-flex;
1561
- align-items: center;
1562
- gap: 4px;
1563
- padding: 2px 8px;
1564
- border-radius: 20px;
1565
- font-size: 10px;
1566
- font-weight: 700;
1567
- background: ${(p) => p.$variant === "verified" ? "#eafbe7" : p.$variant === "banned" ? "#fcecea" : "#f0f0ff"};
1568
- color: ${(p) => p.$variant === "verified" ? "#5cb176" : p.$variant === "banned" ? "#d02b20" : "#8e8ea9"};
1569
-
1570
- &::before {
1571
- content: '';
1572
- display: inline-block;
1573
- width: 5px;
1574
- height: 5px;
1575
- border-radius: 50%;
1576
- background: ${(p) => p.$variant === "verified" ? "#5cb176" : p.$variant === "banned" ? "#d02b20" : "#b8b8c7"};
1577
- }
1578
- `;
1579
- const MonoText = styled__default.default.span`
1580
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
1581
- font-size: 11px;
1582
- color: #8e8ea9;
1583
- `;
1584
- const DateText = styled__default.default.span`
1585
- font-size: 11px;
1586
- color: #b8b8c7;
1587
- font-variant-numeric: tabular-nums;
1588
- `;
1589
- const UserName = styled__default.default.span`
1590
- font-size: 12px;
1591
- font-weight: 600;
1592
- color: #32324d;
1593
- `;
1594
- const Toolbar = styled__default.default.div`
1595
- display: flex;
1596
- align-items: flex-end;
1597
- gap: 12px;
1598
- `;
1599
- function UsersPage({ config }) {
1600
- const rbac = admin.useRBAC(index.PERMISSIONS.user);
1601
- const qc = reactQuery.useQueryClient();
1602
- const { toggleNotification } = admin.useNotification();
1603
- const banEnabled = index.hasPlugin(config, "admin");
1604
- const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
1605
- const twoFactorEnabled = index.hasPlugin(config, "two-factor");
1606
- const [page, setPage] = react.useState(1);
1607
- const [search, setSearch] = react.useState("");
1608
- const [searchInput, setSearchInput] = react.useState("");
1609
- const [selected, setSelected] = react.useState(/* @__PURE__ */ new Set());
1610
- const [showCreate, setShowCreate] = react.useState(false);
1611
- const [detailUserId, setDetailUserId] = react.useState(null);
1612
- const [confirmDelete, setConfirmDelete] = react.useState(null);
1613
- const [confirmDeleteMany, setConfirmDeleteMany] = react.useState(false);
1614
- const [confirmBanMany, setConfirmBanMany] = react.useState(false);
1615
- const offset = (page - 1) * PAGE_SIZE;
1616
- const { data, isLoading, isError, error } = useUsers({
1617
- limit: PAGE_SIZE,
1618
- offset,
1619
- search: search || void 0
1620
- });
1621
- const users = data?.users ?? [];
1622
- const total = data?.total ?? 0;
1623
- const pageCount = Math.ceil(total / PAGE_SIZE);
1624
- const deleteMutation = reactQuery.useMutation({
1625
- mutationFn: async (userId) => {
1626
- const result = await index.client.dash.deleteUser(
1627
- {},
1628
- useModelSchema.withContext({ userId }, index.getAuthHeaders())
1629
- );
1630
- if (result.error)
1631
- throw new Error(result.error.message ?? "Delete failed");
1632
- },
1633
- onSuccess: () => {
1634
- setConfirmDelete(null);
1635
- qc.invalidateQueries({ queryKey: ["dash-users"] });
1636
- qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
1637
- toggleNotification({ type: "success", message: "User deleted" });
1638
- },
1639
- onError: (err) => {
1640
- toggleNotification({
1641
- type: "danger",
1642
- message: err.message ?? "Failed to delete user"
1643
- });
1644
- }
1645
- });
1646
- const deleteManyMutation = reactQuery.useMutation({
1647
- mutationFn: async (userIds) => {
1648
- const result = await index.client.dash.deleteManyUsers(
1649
- {},
1650
- useModelSchema.withContext({ userIds }, index.getAuthHeaders())
1651
- );
1652
- if (result.error)
1653
- throw new Error(result.error.message ?? "Delete failed");
1654
- return result.data;
1655
- },
1656
- onSuccess: (_data, userIds) => {
1657
- setConfirmDeleteMany(false);
1658
- setSelected(/* @__PURE__ */ new Set());
1659
- qc.invalidateQueries({ queryKey: ["dash-users"] });
1660
- qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
1661
- toggleNotification({
1662
- type: "success",
1663
- message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
1664
- });
1665
- },
1666
- onError: (err) => {
1667
- toggleNotification({
1668
- type: "danger",
1669
- message: err.message ?? "Failed to delete users"
1670
- });
1671
- }
1672
- });
1673
- const banManyMutation = reactQuery.useMutation({
1674
- mutationFn: async (userIds) => {
1675
- const result = await index.client.dash.banManyUsers(
1676
- {},
1677
- useModelSchema.withContext({ userIds }, index.getAuthHeaders())
1678
- );
1679
- if (result.error) throw new Error(result.error.message ?? "Ban failed");
1680
- return result.data;
1681
- },
1682
- onSuccess: (_data, userIds) => {
1683
- setConfirmBanMany(false);
1684
- setSelected(/* @__PURE__ */ new Set());
1685
- qc.invalidateQueries({ queryKey: ["dash-users"] });
1686
- toggleNotification({
1687
- type: "success",
1688
- message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
1689
- });
1690
- },
1691
- onError: (err) => {
1692
- toggleNotification({
1693
- type: "danger",
1694
- message: err.message ?? "Failed to ban users"
1695
- });
1696
- }
1697
- });
1698
- const toggleSelect = (id) => {
1699
- setSelected((prev) => {
1700
- const next = new Set(prev);
1701
- if (next.has(id)) next.delete(id);
1702
- else next.add(id);
1703
- return next;
1704
- });
1705
- };
1706
- const allSelected = users.length > 0 && users.every((u) => selected.has(u.id));
1707
- const someSelected = selected.size > 0;
1708
- const handleSelectAll = () => {
1709
- if (allSelected) {
1710
- setSelected(/* @__PURE__ */ new Set());
1711
- } else {
1712
- setSelected(new Set(users.map((u) => u.id)));
1713
- }
1714
- };
1715
- const handleSearch = (e) => {
1716
- e.preventDefault();
1717
- setSearch(searchInput);
1718
- setPage(1);
1719
- };
1720
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap, { "data-testid": "users-page", children: [
1721
- /* @__PURE__ */ jsxRuntime.jsxs(PageHeader, { children: [
1722
- /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock, { children: [
1723
- /* @__PURE__ */ jsxRuntime.jsx(PageTitle, { children: "Users" }),
1724
- /* @__PURE__ */ jsxRuntime.jsxs(PageSubtitle, { children: [
1725
- total.toLocaleString(),
1726
- " total",
1727
- data?.onlineUsers ? ` · ${data.onlineUsers} online` : ""
1728
- ] })
1729
- ] }),
1730
- rbac.allowedActions.canCreate && /* @__PURE__ */ jsxRuntime.jsx(
1731
- designSystem.Button,
1732
- {
1733
- startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
1734
- onClick: () => setShowCreate(true),
1735
- "data-testid": "create-user-btn",
1736
- children: "Create user"
1737
- }
1738
- )
1739
- ] }),
1740
- /* @__PURE__ */ jsxRuntime.jsxs(Toolbar, { children: [
1741
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsxRuntime.jsx(
1742
- designSystem.Searchbar,
1743
- {
1744
- clearLabel: "Clear",
1745
- name: "search",
1746
- placeholder: "Search by email…",
1747
- value: searchInput,
1748
- onChange: (e) => setSearchInput(e.target.value),
1749
- onClear: () => {
1750
- setSearchInput("");
1751
- setSearch("");
1752
- setPage(1);
1753
- },
1754
- "data-testid": "user-search",
1755
- children: "Search users"
1756
- }
1757
- ) }),
1758
- someSelected && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
1759
- rbac.allowedActions.canDelete && /* @__PURE__ */ jsxRuntime.jsxs(
1760
- designSystem.Button,
1761
- {
1762
- variant: "danger-light",
1763
- size: "S",
1764
- onClick: () => setConfirmDeleteMany(true),
1765
- "data-testid": "delete-selected-btn",
1766
- children: [
1767
- "Delete ",
1768
- selected.size,
1769
- " selected"
1770
- ]
1771
- }
1772
- ),
1773
- banEnabled && rbac.allowedActions.canUpdate && /* @__PURE__ */ jsxRuntime.jsxs(
1774
- designSystem.Button,
1775
- {
1776
- variant: "secondary",
1777
- size: "S",
1778
- onClick: () => setConfirmBanMany(true),
1779
- children: [
1780
- "Ban ",
1781
- selected.size,
1782
- " selected"
1783
- ]
1784
- }
1785
- )
1786
- ] })
1787
- ] }),
1788
- isError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: error?.message }),
1789
- /* @__PURE__ */ jsxRuntime.jsx(TableCard, { children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading users…" }) }) : /* @__PURE__ */ jsxRuntime.jsxs(Table, { children: [
1790
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1791
- (rbac.allowedActions.canDelete || rbac.allowedActions.canUpdate) && /* @__PURE__ */ jsxRuntime.jsx(THCheck, { children: /* @__PURE__ */ jsxRuntime.jsx(
1792
- designSystem.Checkbox,
1793
- {
1794
- checked: allSelected,
1795
- onCheckedChange: handleSelectAll,
1796
- "aria-label": "Select all"
1797
- }
1798
- ) }),
1799
- /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Name" }),
1800
- /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Email" }),
1801
- /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Status" }),
1802
- /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Created" }),
1803
- /* @__PURE__ */ jsxRuntime.jsx(THActions, {})
1804
- ] }) }),
1805
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
1806
- TD,
1807
- {
1808
- colSpan: 6,
1809
- style: {
1810
- textAlign: "center",
1811
- padding: "40px",
1812
- color: "#8e8ea9"
1813
- },
1814
- "data-testid": "users-empty",
1815
- children: search ? `No users matching "${search}"` : "No users found"
1816
- }
1817
- ) }) : users.map((user, i) => /* @__PURE__ */ jsxRuntime.jsxs(
1818
- TR,
1819
- {
1820
- $selected: selected.has(user.id),
1821
- $i: i,
1822
- "data-testid": "user-row",
1823
- children: [
1824
- (rbac.allowedActions.canDelete || rbac.allowedActions.canUpdate) && /* @__PURE__ */ jsxRuntime.jsx(TDCheck, { children: /* @__PURE__ */ jsxRuntime.jsx(
1825
- designSystem.Checkbox,
1826
- {
1827
- checked: selected.has(user.id),
1828
- onCheckedChange: () => toggleSelect(user.id),
1829
- "aria-label": `Select ${user.name}`
1830
- }
1831
- ) }),
1832
- /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
1833
- /* @__PURE__ */ jsxRuntime.jsx(
1834
- Avatar.Avatar,
1835
- {
1836
- name: user.name ?? "",
1837
- src: user.image,
1838
- size: 28
1839
- }
1840
- ),
1841
- /* @__PURE__ */ jsxRuntime.jsx(UserName, { children: user.name })
1842
- ] }) }),
1843
- /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsx(MonoText, { children: user.email }) }),
1844
- /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, children: [
1845
- user.emailVerified ? /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { $variant: "verified", children: "Verified" }) : /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { $variant: "unverified", children: "Unverified" }),
1846
- user.banned && /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { $variant: "banned", children: "Banned" })
1847
- ] }) }),
1848
- /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsx(DateText, { children: new Date(user.createdAt).toLocaleDateString() }) }),
1849
- /* @__PURE__ */ jsxRuntime.jsx(TDActions, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, justifyContent: "flex-end", children: [
1850
- rbac.allowedActions.canUpdate && /* @__PURE__ */ jsxRuntime.jsx(
1851
- designSystem.IconButton,
1852
- {
1853
- label: "Edit user",
1854
- onClick: () => setDetailUserId(user.id),
1855
- "data-testid": "edit-user-btn",
1856
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, {})
1857
- }
1858
- ),
1859
- rbac.allowedActions.canDelete && /* @__PURE__ */ jsxRuntime.jsx(
1860
- designSystem.IconButton,
1861
- {
1862
- label: "Delete user",
1863
- onClick: () => setConfirmDelete(user.id),
1864
- "data-testid": "delete-user-btn",
1865
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
1866
- }
1867
- )
1868
- ] }) })
1869
- ]
1870
- },
1871
- user.id
1872
- )) })
1873
- ] }) }),
1874
- pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
1875
- /* @__PURE__ */ jsxRuntime.jsx(
1876
- designSystem.Button,
1877
- {
1878
- variant: "tertiary",
1879
- size: "S",
1880
- disabled: page === 1,
1881
- onClick: () => setPage((p) => p - 1),
1882
- children: "Previous"
1883
- }
1884
- ),
1885
- /* @__PURE__ */ jsxRuntime.jsx(
1886
- designSystem.Button,
1887
- {
1888
- variant: "tertiary",
1889
- size: "S",
1890
- disabled: page >= pageCount,
1891
- onClick: () => setPage((p) => p + 1),
1892
- children: "Next"
1893
- }
1894
- )
1895
- ] }) }),
1896
- rbac.allowedActions.canCreate && showCreate && /* @__PURE__ */ jsxRuntime.jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
1897
- rbac.allowedActions.canUpdate && detailUserId && /* @__PURE__ */ jsxRuntime.jsx(
1898
- UserDetailDrawer,
1899
- {
1900
- userId: detailUserId,
1901
- banEnabled,
1902
- emailVerificationEnabled,
1903
- twoFactorEnabled,
1904
- onClose: () => setDetailUserId(null)
1905
- }
1906
- ),
1907
- rbac.allowedActions.canDelete && confirmDelete && /* @__PURE__ */ jsxRuntime.jsx(
1908
- useModelSchema.ConfirmDialog,
1909
- {
1910
- title: "Delete user",
1911
- message: "Are you sure you want to delete this user? This action cannot be undone.",
1912
- confirmLabel: "Delete",
1913
- loading: deleteMutation.isLoading,
1914
- onConfirm: () => deleteMutation.mutate(confirmDelete),
1915
- onCancel: () => setConfirmDelete(null)
1916
- }
1917
- ),
1918
- rbac.allowedActions.canDelete && confirmDeleteMany && /* @__PURE__ */ jsxRuntime.jsx(
1919
- useModelSchema.ConfirmDialog,
1920
- {
1921
- title: `Delete ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
1922
- message: `Are you sure you want to delete ${selected.size} user${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
1923
- confirmLabel: "Delete all",
1924
- loading: deleteManyMutation.isLoading,
1925
- onConfirm: () => deleteManyMutation.mutate([...selected]),
1926
- onCancel: () => setConfirmDeleteMany(false)
1927
- }
1928
- ),
1929
- rbac.allowedActions.canUpdate && confirmBanMany && /* @__PURE__ */ jsxRuntime.jsx(
1930
- useModelSchema.ConfirmDialog,
1931
- {
1932
- title: `Ban ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
1933
- message: `Are you sure you want to ban ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be prevented from signing in.`,
1934
- confirmLabel: "Ban all",
1935
- variant: "danger",
1936
- loading: banManyMutation.isLoading,
1937
- onConfirm: () => banManyMutation.mutate([...selected]),
1938
- onCancel: () => setConfirmBanMany(false)
1939
- }
1940
- )
1941
- ] });
1942
- }
1943
- function ProtectedUsersPage() {
1944
- const { data: config, isError, error } = index.useDashConfig();
1945
- if (isError) {
1946
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(
1947
- designSystem.Alert,
1948
- {
1949
- closeLabel: "Close",
1950
- title: "Error loading configuration",
1951
- variant: "danger",
1952
- children: error.message
1953
- }
1954
- ) });
1955
- }
1956
- if (!config) {
1957
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading user configuration…" }) });
1958
- }
1959
- return /* @__PURE__ */ jsxRuntime.jsx(
1960
- admin.Page.Protect,
1961
- {
1962
- permissions: index.PERMISSIONS.user.filter(
1963
- (p) => p.action === `plugin::${index.PLUGIN_ID}.user.read`
1964
- ),
1965
- children: /* @__PURE__ */ jsxRuntime.jsx(UsersPage, { config })
1966
- }
1967
- );
1968
- }
1969
- exports.UsersPage = UsersPage;
1970
- exports.default = ProtectedUsersPage;