@strapi-community/plugin-better-auth-dashboard 1.0.0-alpha.9 → 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 (42) hide show
  1. package/README.md +65 -0
  2. package/dist/admin/Avatar-CdT7eBCh.js +39 -0
  3. package/dist/admin/Avatar-zlMTAZh3.mjs +40 -0
  4. package/dist/admin/Root-CgVBJuoM.mjs +365 -0
  5. package/dist/admin/Root-CsiOnzlB.js +367 -0
  6. package/dist/admin/cs-C_jSgeON.js +416 -0
  7. package/dist/admin/cs-wtnknvBC.mjs +416 -0
  8. package/dist/admin/de-Cg5HyoCY.js +416 -0
  9. package/dist/admin/de-DnnVdUxk.mjs +416 -0
  10. package/dist/admin/en-B3utEgMT.mjs +416 -0
  11. package/dist/admin/en-Cz1IJEvp.js +416 -0
  12. package/dist/admin/es-CGA0Tslt.js +416 -0
  13. package/dist/admin/es-RoCEriJL.mjs +416 -0
  14. package/dist/admin/fr-Ca74t21t.js +416 -0
  15. package/dist/admin/fr-CtmuSfiQ.mjs +416 -0
  16. package/dist/admin/index-BCBu-0nr.js +457 -0
  17. package/dist/admin/index-BI2WIsds.mjs +458 -0
  18. package/dist/admin/index-CPSoAbZt.js +2321 -0
  19. package/dist/admin/index-D9ttdrT3.js +2716 -0
  20. package/dist/admin/index-DQdn5s8q.js +1184 -0
  21. package/dist/admin/index-DvfwvCj0.mjs +1182 -0
  22. package/dist/admin/index-VyHXoQ6p.mjs +2321 -0
  23. package/dist/admin/index-gFqHlMLP.mjs +2716 -0
  24. package/dist/admin/index.js +2 -2
  25. package/dist/admin/index.mjs +2 -2
  26. package/dist/admin/it-BAB9hDdp.mjs +416 -0
  27. package/dist/admin/it-dYT2_ERV.js +416 -0
  28. package/dist/admin/nl-CVqSbUU9.js +416 -0
  29. package/dist/admin/nl-CYv26PHv.mjs +416 -0
  30. package/dist/admin/pl-Bc0cS2mD.mjs +416 -0
  31. package/dist/admin/pl-HYF4BxRv.js +416 -0
  32. package/dist/admin/pt-DDUGyLtb.js +416 -0
  33. package/dist/admin/pt-DZyyK3EG.mjs +416 -0
  34. package/dist/admin/useModelSchema-BogQah8f.js +756 -0
  35. package/dist/admin/useModelSchema-C6bxNjW4.mjs +755 -0
  36. package/dist/server/index.js +220 -10
  37. package/dist/server/index.mjs +220 -10
  38. package/package.json +5 -4
  39. package/dist/admin/Root-BYdYQBE5.mjs +0 -5742
  40. package/dist/admin/Root-D_vCtQoC.js +0 -5744
  41. package/dist/admin/index-BMTIuxZI.mjs +0 -96
  42. package/dist/admin/index-CkSsWnBx.js +0 -95
@@ -0,0 +1,2716 @@
1
+ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
+ import { Button, Flex, Field, TextInput, Checkbox, Box, Typography, Badge, Loader, Alert, Tabs, Grid, NumberInput, IconButton, Modal, Main, SearchForm, Searchbar, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td } from "@strapi/design-system";
3
+ import { Trash, Plus, Pencil } from "@strapi/icons";
4
+ import { useNotification, useFetchClient, Page, useRBAC, Layouts } from "@strapi/strapi/admin";
5
+ import { useState } from "react";
6
+ import { useIntl } from "react-intl";
7
+ import { useQuery, useQueryClient, useMutation } from "react-query";
8
+ import { d as client, g as getTranslation, e as getAuthHeaders, u as useDashConfig, c as PERMISSIONS, P as PLUGIN_ID, h as hasPlugin } from "./index-BI2WIsds.mjs";
9
+ import { A as Avatar } from "./Avatar-zlMTAZh3.mjs";
10
+ import { S as SheetDialog, F as FormSection, a as SectionLabel, M as MediaPickerField, u as useModelSchema, w as withContext, E as EditLayout, C as CustomFieldsSection, b as EditSidebar, c as MetaItem, d as MetaKey, e as MonoChip, f as MetaVal, g as EditViewSidePanels, A as AccountRow, P as ProviderBadge, D as DangerRow, W as WarnCard, h as PreviewPill, R as RoleBadge, i as ConfirmDialog, j as Code } from "./useModelSchema-C6bxNjW4.mjs";
11
+ function useUsers(options = {}) {
12
+ const { limit = 25, offset = 0, search } = options;
13
+ const { formatMessage } = useIntl();
14
+ return useQuery({
15
+ queryKey: ["dash-users", limit, offset, search],
16
+ queryFn: async () => {
17
+ const whereClause = search ? JSON.stringify([
18
+ {
19
+ field: "email",
20
+ operator: "contains",
21
+ value: search,
22
+ connector: "OR"
23
+ },
24
+ {
25
+ field: "name",
26
+ operator: "contains",
27
+ value: search,
28
+ connector: "OR"
29
+ }
30
+ ]) : void 0;
31
+ const result = await client.dash.listUsers({
32
+ query: {
33
+ limit,
34
+ offset,
35
+ sortBy: "createdAt",
36
+ sortOrder: "desc",
37
+ ...whereClause ? { where: whereClause } : {}
38
+ }
39
+ });
40
+ if (result.error) {
41
+ throw new Error(
42
+ result.error.message ?? formatMessage({
43
+ id: getTranslation("common.errors.loadUsers"),
44
+ defaultMessage: "Failed to load users"
45
+ })
46
+ );
47
+ }
48
+ return result.data;
49
+ },
50
+ keepPreviousData: true
51
+ });
52
+ }
53
+ function CreateUserDialog({ onClose }) {
54
+ const { formatMessage } = useIntl();
55
+ const qc = useQueryClient();
56
+ const { toggleNotification } = useNotification();
57
+ const [name, setName] = useState("");
58
+ const [email, setEmail] = useState("");
59
+ const [image, setImage] = useState("");
60
+ const [password, setPassword] = useState("");
61
+ const [generatePassword, setGeneratePassword] = useState(false);
62
+ const [emailVerified, setEmailVerified] = useState(false);
63
+ const [sendVerificationEmail, setSendVerificationEmail] = useState(false);
64
+ const createMutation = useMutation({
65
+ mutationFn: async () => {
66
+ const result = await client.dash.createUser({
67
+ name,
68
+ email,
69
+ ...image ? { image } : {},
70
+ ...generatePassword ? { generatePassword: true } : password ? { password } : {},
71
+ emailVerified,
72
+ sendVerificationEmail
73
+ });
74
+ if (result.error)
75
+ throw new Error(
76
+ result.error.message ?? formatMessage({
77
+ id: getTranslation("users.create.errors.createFailed"),
78
+ defaultMessage: "Create failed"
79
+ })
80
+ );
81
+ return result.data;
82
+ },
83
+ onSuccess: () => {
84
+ qc.invalidateQueries({ queryKey: ["dash-users"] });
85
+ qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
86
+ toggleNotification({
87
+ type: "success",
88
+ message: formatMessage({
89
+ id: getTranslation("users.create.notifications.success"),
90
+ defaultMessage: "User created successfully"
91
+ })
92
+ });
93
+ onClose();
94
+ },
95
+ onError: (err) => {
96
+ toggleNotification({
97
+ type: "danger",
98
+ message: err.message ?? formatMessage({
99
+ id: getTranslation("users.create.notifications.error"),
100
+ defaultMessage: "Failed to create user"
101
+ })
102
+ });
103
+ }
104
+ });
105
+ const footer = /* @__PURE__ */ jsxs(Fragment, { children: [
106
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: onClose, children: formatMessage({
107
+ id: getTranslation("users.create.cancel"),
108
+ defaultMessage: "Cancel"
109
+ }) }),
110
+ /* @__PURE__ */ jsx(
111
+ Button,
112
+ {
113
+ loading: createMutation.isLoading,
114
+ onClick: () => createMutation.mutate(),
115
+ disabled: !name || !email,
116
+ "data-testid": "create-user-submit",
117
+ children: formatMessage({
118
+ id: getTranslation("users.create.submit"),
119
+ defaultMessage: "Create user"
120
+ })
121
+ }
122
+ )
123
+ ] });
124
+ return /* @__PURE__ */ jsx(
125
+ SheetDialog,
126
+ {
127
+ title: formatMessage({
128
+ id: getTranslation("users.create.title"),
129
+ defaultMessage: "Create user"
130
+ }),
131
+ actionButtons: footer,
132
+ onClose,
133
+ "data-testid": "create-user-drawer",
134
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, children: [
135
+ /* @__PURE__ */ jsxs(FormSection, { children: [
136
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
137
+ id: getTranslation("users.create.sections.personal"),
138
+ defaultMessage: "Personal information"
139
+ }) }),
140
+ /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, required: true, children: [
141
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
142
+ id: getTranslation("users.create.fields.name.label"),
143
+ defaultMessage: "Full name"
144
+ }) }),
145
+ /* @__PURE__ */ jsx(
146
+ TextInput,
147
+ {
148
+ name: "name",
149
+ value: name,
150
+ onChange: (e) => setName(e.target.value),
151
+ placeholder: formatMessage({
152
+ id: getTranslation("users.create.fields.name.placeholder"),
153
+ defaultMessage: "Jane Smith"
154
+ }),
155
+ "data-testid": "new-user-name"
156
+ }
157
+ )
158
+ ] }),
159
+ /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, required: true, children: [
160
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
161
+ id: getTranslation("users.create.fields.email.label"),
162
+ defaultMessage: "Email address"
163
+ }) }),
164
+ /* @__PURE__ */ jsx(
165
+ TextInput,
166
+ {
167
+ name: "email",
168
+ type: "email",
169
+ value: email,
170
+ onChange: (e) => setEmail(e.target.value),
171
+ placeholder: formatMessage({
172
+ id: getTranslation("users.create.fields.email.placeholder"),
173
+ defaultMessage: "jane@example.com"
174
+ }),
175
+ "data-testid": "new-user-email"
176
+ }
177
+ )
178
+ ] }),
179
+ /* @__PURE__ */ jsx(
180
+ MediaPickerField,
181
+ {
182
+ label: formatMessage({
183
+ id: getTranslation("users.create.fields.image.label"),
184
+ defaultMessage: "Avatar URL"
185
+ }),
186
+ name: "image",
187
+ value: image,
188
+ onChange: setImage,
189
+ placeholder: formatMessage({
190
+ id: getTranslation("users.create.fields.image.placeholder"),
191
+ defaultMessage: "https://example.com/avatar.png"
192
+ }),
193
+ hint: formatMessage({
194
+ id: getTranslation("users.create.fields.image.hint"),
195
+ defaultMessage: "Optional — publicly accessible image URL"
196
+ })
197
+ }
198
+ )
199
+ ] }),
200
+ /* @__PURE__ */ jsxs(FormSection, { children: [
201
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
202
+ id: getTranslation("users.create.sections.password"),
203
+ defaultMessage: "Password"
204
+ }) }),
205
+ /* @__PURE__ */ jsx(
206
+ Checkbox,
207
+ {
208
+ name: "generatePassword",
209
+ checked: generatePassword,
210
+ onCheckedChange: (checked) => {
211
+ setGeneratePassword(checked);
212
+ if (checked) setPassword("");
213
+ },
214
+ children: formatMessage({
215
+ id: getTranslation("users.create.fields.generatePassword"),
216
+ defaultMessage: "Generate a random password"
217
+ })
218
+ }
219
+ ),
220
+ !generatePassword && /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, children: [
221
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
222
+ id: getTranslation("users.create.fields.password.label"),
223
+ defaultMessage: "Password"
224
+ }) }),
225
+ /* @__PURE__ */ jsx(
226
+ TextInput,
227
+ {
228
+ name: "password",
229
+ type: "password",
230
+ value: password,
231
+ onChange: (e) => setPassword(e.target.value),
232
+ placeholder: formatMessage({
233
+ id: getTranslation(
234
+ "users.create.fields.password.placeholder"
235
+ ),
236
+ defaultMessage: "Leave blank for a passwordless account"
237
+ })
238
+ }
239
+ )
240
+ ] })
241
+ ] }),
242
+ /* @__PURE__ */ jsxs(FormSection, { children: [
243
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
244
+ id: getTranslation("users.create.sections.emailVerification"),
245
+ defaultMessage: "Email verification"
246
+ }) }),
247
+ /* @__PURE__ */ jsx(
248
+ Checkbox,
249
+ {
250
+ name: "emailVerified",
251
+ checked: emailVerified,
252
+ onCheckedChange: (checked) => {
253
+ setEmailVerified(checked);
254
+ if (checked) setSendVerificationEmail(false);
255
+ },
256
+ children: formatMessage({
257
+ id: getTranslation("users.create.fields.emailVerified"),
258
+ defaultMessage: "Mark email as already verified"
259
+ })
260
+ }
261
+ ),
262
+ !emailVerified && /* @__PURE__ */ jsx(
263
+ Checkbox,
264
+ {
265
+ name: "sendVerificationEmail",
266
+ checked: sendVerificationEmail,
267
+ onCheckedChange: (checked) => setSendVerificationEmail(checked),
268
+ children: formatMessage({
269
+ id: getTranslation("users.create.fields.sendVerificationEmail"),
270
+ defaultMessage: "Send a verification email to the user"
271
+ })
272
+ }
273
+ ),
274
+ emailVerified && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "success600", children: formatMessage({
275
+ id: getTranslation("users.create.fields.emailVerifiedHint"),
276
+ defaultMessage: "The user will be able to sign in immediately without verifying their email."
277
+ }) }) })
278
+ ] })
279
+ ] })
280
+ }
281
+ );
282
+ }
283
+ function usePluginSettings() {
284
+ const { get } = useFetchClient();
285
+ return useQuery({
286
+ queryKey: ["dash-plugin-settings"],
287
+ queryFn: async () => {
288
+ const { data } = await get(
289
+ "/better-auth-dashboard/settings"
290
+ );
291
+ return data;
292
+ },
293
+ staleTime: 5 * 60 * 1e3
294
+ });
295
+ }
296
+ function ReadOnlyField({ label, value }) {
297
+ return /* @__PURE__ */ jsxs(Field.Root, { width: "100%", children: [
298
+ /* @__PURE__ */ jsx(Field.Label, { children: label }),
299
+ /* @__PURE__ */ jsx(TextInput, { readOnly: true, value })
300
+ ] });
301
+ }
302
+ function BackupCodes({ codes }) {
303
+ return /* @__PURE__ */ jsx(Grid.Root, { gap: 2, gridCols: 2, children: codes.map((code) => /* @__PURE__ */ jsx(Grid.Item, { col: 1, children: /* @__PURE__ */ jsx(
304
+ Box,
305
+ {
306
+ width: "100%",
307
+ background: "neutral0",
308
+ borderColor: "neutral200",
309
+ borderStyle: "solid",
310
+ borderWidth: "1px",
311
+ hasRadius: true,
312
+ padding: 2,
313
+ textAlign: "center",
314
+ children: /* @__PURE__ */ jsx(Code, { variant: "omega", fontWeight: "semiBold", textColor: "neutral800", children: code })
315
+ }
316
+ ) }, code)) });
317
+ }
318
+ const STANDARD_FIELDS = /* @__PURE__ */ new Set([
319
+ "id",
320
+ "name",
321
+ "email",
322
+ "emailVerified",
323
+ "image",
324
+ "banned",
325
+ "banReason",
326
+ "banExpires",
327
+ "createdAt",
328
+ "updatedAt"
329
+ ]);
330
+ function UserDetailDrawer({
331
+ userId,
332
+ banEnabled,
333
+ emailVerificationEnabled,
334
+ twoFactorEnabled,
335
+ onClose
336
+ }) {
337
+ const { formatMessage } = useIntl();
338
+ const qc = useQueryClient();
339
+ const { toggleNotification } = useNotification();
340
+ const { get, put } = useFetchClient();
341
+ const schemaQuery = useModelSchema("user");
342
+ const settingsQuery = usePluginSettings();
343
+ const emailCallbackUrl = settingsQuery.data?.email_callback_url ?? null;
344
+ const userQuery = useQuery({
345
+ queryKey: ["dash-user", userId],
346
+ queryFn: async () => {
347
+ const result = await client.dash.user(
348
+ {},
349
+ withContext({ userId }, getAuthHeaders())
350
+ );
351
+ if (result.error)
352
+ throw new Error(
353
+ result.error.message ?? formatMessage({
354
+ id: getTranslation("userDetail.common.loadUserFailed"),
355
+ defaultMessage: "Failed to load user"
356
+ })
357
+ );
358
+ return result.data;
359
+ }
360
+ });
361
+ const orgsQuery = useQuery({
362
+ queryKey: ["dash-user-orgs", userId],
363
+ queryFn: async () => {
364
+ const result = await client.dash.userOrganizations(
365
+ {},
366
+ withContext({ userId }, getAuthHeaders())
367
+ );
368
+ if (result.error)
369
+ throw new Error(
370
+ result.error.message ?? formatMessage({
371
+ id: getTranslation("userDetail.common.failed"),
372
+ defaultMessage: "Failed"
373
+ })
374
+ );
375
+ return result.data?.organizations ?? [];
376
+ }
377
+ });
378
+ const sessionsQuery = useQuery({
379
+ queryKey: ["dash-user-sessions", userId],
380
+ queryFn: async () => {
381
+ const { data } = await get(
382
+ `/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
383
+ );
384
+ return data.results ?? [];
385
+ }
386
+ });
387
+ const strapiUserQuery = useQuery({
388
+ queryKey: ["dash-strapi-user", userId],
389
+ enabled: !!schemaQuery.data,
390
+ queryFn: async () => {
391
+ const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
392
+ const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
393
+ const { data } = await get(
394
+ `/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
395
+ );
396
+ return data.results?.[0] ?? null;
397
+ }
398
+ });
399
+ const [activeTab, setActiveTab] = useState("profile");
400
+ const [editName, setEditName] = useState(void 0);
401
+ const [editEmail, setEditEmail] = useState(void 0);
402
+ const [editEmailVerified, setEditEmailVerified] = useState(void 0);
403
+ const [editImage, setEditImage] = useState(void 0);
404
+ const [editExtra, setEditExtra] = useState({});
405
+ const [newPassword, setNewPassword] = useState("");
406
+ const [banReason, setBanReason] = useState("");
407
+ const [banExpiresDays, setBanExpiresDays] = useState("");
408
+ const [confirmRevokeAll, setConfirmRevokeAll] = useState(false);
409
+ const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
410
+ const [confirmBan, setConfirmBan] = useState(false);
411
+ const [confirmUnban, setConfirmUnban] = useState(false);
412
+ const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = useState(null);
413
+ const [confirmDisable2FA, setConfirmDisable2FA] = useState(false);
414
+ const [showTotpModal, setShowTotpModal] = useState(false);
415
+ const [showBackupCodesModal, setShowBackupCodesModal] = useState(false);
416
+ const [confirmRegenerateBackupCodes, setConfirmRegenerateBackupCodes] = useState(false);
417
+ const [twoFactorData, setTwoFactorData] = useState(null);
418
+ const [totpUriData, setTotpUriData] = useState(null);
419
+ const [backupCodesData, setBackupCodesData] = useState(null);
420
+ const user = userQuery.data;
421
+ const displayName = editName ?? user?.name ?? "";
422
+ const displayEmail = editEmail ?? user?.email ?? "";
423
+ const displayEmailVerified = editEmailVerified ?? user?.emailVerified ?? false;
424
+ const displayImage = editImage ?? user?.image ?? "";
425
+ const handleExtraChange = (name, value) => {
426
+ setEditExtra((prev) => ({ ...prev, [name]: value }));
427
+ };
428
+ const extraData = {
429
+ ...strapiUserQuery.data ?? {},
430
+ ...editExtra
431
+ };
432
+ const updateMutation = useMutation({
433
+ mutationFn: async () => {
434
+ const body = {};
435
+ if (editName !== void 0) body.name = editName;
436
+ if (editEmail !== void 0) body.email = editEmail;
437
+ if (editEmailVerified !== void 0)
438
+ body.emailVerified = editEmailVerified;
439
+ if (editImage !== void 0) body.image = editImage;
440
+ const documentId = strapiUserQuery.data?.documentId;
441
+ if (!documentId)
442
+ throw new Error(
443
+ formatMessage({
444
+ id: getTranslation("userDetail.errors.documentIdMissing"),
445
+ defaultMessage: "Could not resolve documentId for user"
446
+ })
447
+ );
448
+ const updateUser = await client.dash.updateUser(
449
+ { userId, ...body },
450
+ withContext({ userId }, getAuthHeaders())
451
+ );
452
+ if (!updateUser.data)
453
+ throw new Error(
454
+ formatMessage({
455
+ id: getTranslation("userDetail.common.updateFailed"),
456
+ defaultMessage: "Update failed"
457
+ })
458
+ );
459
+ if (Object.keys(editExtra).length > 0) {
460
+ await put(
461
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
462
+ editExtra
463
+ );
464
+ }
465
+ },
466
+ onSuccess: async () => {
467
+ await qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
468
+ qc.invalidateQueries({ queryKey: ["dash-user", userId] });
469
+ qc.invalidateQueries({ queryKey: ["dash-users"] });
470
+ setEditName(void 0);
471
+ setEditEmail(void 0);
472
+ setEditEmailVerified(void 0);
473
+ setEditImage(void 0);
474
+ setEditExtra({});
475
+ toggleNotification({
476
+ type: "success",
477
+ message: formatMessage({
478
+ id: getTranslation("userDetail.notifications.updated"),
479
+ defaultMessage: "User updated successfully"
480
+ })
481
+ });
482
+ onClose();
483
+ },
484
+ onError: (err) => {
485
+ toggleNotification({
486
+ type: "danger",
487
+ message: err.message ?? formatMessage({
488
+ id: getTranslation("userDetail.common.updateFailed"),
489
+ defaultMessage: "Update failed"
490
+ })
491
+ });
492
+ }
493
+ });
494
+ const revokeSessionMutation = useMutation({
495
+ mutationFn: async (sessionId) => {
496
+ const result = await client.dash.sessions.revoke(
497
+ {},
498
+ withContext({ sessionId, userId }, getAuthHeaders())
499
+ );
500
+ if (result.error)
501
+ throw new Error(
502
+ result.error.message ?? formatMessage({
503
+ id: getTranslation("userDetail.common.revokeFailed"),
504
+ defaultMessage: "Revoke failed"
505
+ })
506
+ );
507
+ },
508
+ onSuccess: () => {
509
+ setConfirmRevokeSessionId(null);
510
+ qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
511
+ toggleNotification({
512
+ type: "success",
513
+ message: formatMessage({
514
+ id: getTranslation("userDetail.notifications.sessionRevoked"),
515
+ defaultMessage: "Session revoked"
516
+ })
517
+ });
518
+ },
519
+ onError: (err) => {
520
+ toggleNotification({
521
+ type: "danger",
522
+ message: err.message ?? formatMessage({
523
+ id: getTranslation("userDetail.notifications.revokeSessionError"),
524
+ defaultMessage: "Failed to revoke session"
525
+ })
526
+ });
527
+ }
528
+ });
529
+ const passwordMutation = useMutation({
530
+ mutationFn: async () => {
531
+ const result = await client.dash.setPassword(
532
+ { password: newPassword },
533
+ withContext({ userId }, getAuthHeaders())
534
+ );
535
+ if (result.error)
536
+ throw new Error(
537
+ result.error.message ?? formatMessage({
538
+ id: getTranslation("userDetail.common.passwordUpdateFailed"),
539
+ defaultMessage: "Password update failed"
540
+ })
541
+ );
542
+ return result.data;
543
+ },
544
+ onSuccess: () => {
545
+ setNewPassword("");
546
+ toggleNotification({
547
+ type: "success",
548
+ message: formatMessage({
549
+ id: getTranslation("userDetail.notifications.passwordUpdated"),
550
+ defaultMessage: "Password updated successfully"
551
+ })
552
+ });
553
+ },
554
+ onError: (err) => {
555
+ toggleNotification({
556
+ type: "danger",
557
+ message: err.message ?? formatMessage({
558
+ id: getTranslation("userDetail.common.passwordUpdateFailed"),
559
+ defaultMessage: "Password update failed"
560
+ })
561
+ });
562
+ }
563
+ });
564
+ const banMutation = useMutation({
565
+ mutationFn: async () => {
566
+ const body = {};
567
+ if (banReason) body.banReason = banReason;
568
+ if (banExpiresDays) {
569
+ const days = parseInt(banExpiresDays, 10);
570
+ if (!Number.isNaN(days)) body.banExpires = Date.now() + days * 864e5;
571
+ }
572
+ const result = await client.dash.banUser(
573
+ body,
574
+ withContext({ userId }, getAuthHeaders())
575
+ );
576
+ if (result.error)
577
+ throw new Error(
578
+ result.error.message ?? formatMessage({
579
+ id: getTranslation("userDetail.common.banFailed"),
580
+ defaultMessage: "Ban failed"
581
+ })
582
+ );
583
+ return result.data;
584
+ },
585
+ onSuccess: () => {
586
+ setConfirmBan(false);
587
+ qc.invalidateQueries({ queryKey: ["dash-user", userId] });
588
+ qc.invalidateQueries({ queryKey: ["dash-users"] });
589
+ setBanReason("");
590
+ setBanExpiresDays("");
591
+ toggleNotification({
592
+ type: "success",
593
+ message: formatMessage({
594
+ id: getTranslation("userDetail.notifications.banned"),
595
+ defaultMessage: "User has been banned"
596
+ })
597
+ });
598
+ },
599
+ onError: (err) => {
600
+ toggleNotification({
601
+ type: "danger",
602
+ message: err.message ?? formatMessage({
603
+ id: getTranslation("userDetail.common.banFailed"),
604
+ defaultMessage: "Ban failed"
605
+ })
606
+ });
607
+ }
608
+ });
609
+ const unbanMutation = useMutation({
610
+ mutationFn: async () => {
611
+ const result = await client.dash.unbanUser(
612
+ {},
613
+ withContext({ userId }, getAuthHeaders())
614
+ );
615
+ if (result.error)
616
+ throw new Error(
617
+ result.error.message ?? formatMessage({
618
+ id: getTranslation("userDetail.common.unbanFailed"),
619
+ defaultMessage: "Unban failed"
620
+ })
621
+ );
622
+ return result.data;
623
+ },
624
+ onSuccess: () => {
625
+ setConfirmUnban(false);
626
+ qc.invalidateQueries({ queryKey: ["dash-user", userId] });
627
+ qc.invalidateQueries({ queryKey: ["dash-users"] });
628
+ toggleNotification({
629
+ type: "success",
630
+ message: formatMessage({
631
+ id: getTranslation("userDetail.notifications.unbanned"),
632
+ defaultMessage: "Ban has been lifted"
633
+ })
634
+ });
635
+ },
636
+ onError: (err) => {
637
+ toggleNotification({
638
+ type: "danger",
639
+ message: err.message ?? formatMessage({
640
+ id: getTranslation("userDetail.common.unbanFailed"),
641
+ defaultMessage: "Unban failed"
642
+ })
643
+ });
644
+ }
645
+ });
646
+ const unlinkAccountMutation = useMutation({
647
+ mutationFn: async (providerId) => {
648
+ const result = await client.dash.unlinkAccount(
649
+ { providerId },
650
+ withContext({ userId }, getAuthHeaders())
651
+ );
652
+ if (result.error)
653
+ throw new Error(
654
+ result.error.message ?? formatMessage({
655
+ id: getTranslation("userDetail.common.unlinkFailed"),
656
+ defaultMessage: "Failed to unlink account"
657
+ })
658
+ );
659
+ },
660
+ onSuccess: () => {
661
+ setConfirmUnlinkAccountId(null);
662
+ qc.invalidateQueries({ queryKey: ["dash-user", userId] });
663
+ toggleNotification({
664
+ type: "success",
665
+ message: formatMessage({
666
+ id: getTranslation("userDetail.notifications.accountUnlinked"),
667
+ defaultMessage: "Account unlinked"
668
+ })
669
+ });
670
+ },
671
+ onError: (err) => {
672
+ toggleNotification({
673
+ type: "danger",
674
+ message: err.message ?? formatMessage({
675
+ id: getTranslation("userDetail.common.unlinkFailed"),
676
+ defaultMessage: "Failed to unlink account"
677
+ })
678
+ });
679
+ }
680
+ });
681
+ const revokeAllMutation = useMutation({
682
+ mutationFn: async () => {
683
+ const result = await client.dash.sessions.revokeAll(
684
+ { userId },
685
+ withContext({}, getAuthHeaders())
686
+ );
687
+ if (result.error)
688
+ throw new Error(
689
+ result.error.message ?? formatMessage({
690
+ id: getTranslation("userDetail.common.revokeFailed"),
691
+ defaultMessage: "Revoke failed"
692
+ })
693
+ );
694
+ return result.data;
695
+ },
696
+ onSuccess: () => {
697
+ setConfirmRevokeAll(false);
698
+ toggleNotification({
699
+ type: "success",
700
+ message: formatMessage({
701
+ id: getTranslation("userDetail.notifications.allSessionsRevoked"),
702
+ defaultMessage: "All sessions revoked"
703
+ })
704
+ });
705
+ },
706
+ onError: (err) => {
707
+ toggleNotification({
708
+ type: "danger",
709
+ message: err.message ?? formatMessage({
710
+ id: getTranslation("userDetail.notifications.revokeAllError"),
711
+ defaultMessage: "Failed to revoke sessions"
712
+ })
713
+ });
714
+ }
715
+ });
716
+ const sendVerificationMutation = useMutation({
717
+ mutationFn: async () => {
718
+ if (!emailCallbackUrl) {
719
+ throw new Error(
720
+ formatMessage({
721
+ id: getTranslation("userDetail.common.callbackUrlMissing"),
722
+ defaultMessage: "Set `email_callback_url` in the better-auth-dashboard plugin config to enable this action."
723
+ })
724
+ );
725
+ }
726
+ const result = await client.dash.sendVerificationEmail(
727
+ { callbackUrl: emailCallbackUrl },
728
+ withContext({ userId }, getAuthHeaders())
729
+ );
730
+ if (result.error)
731
+ throw new Error(
732
+ result.error.message ?? formatMessage({
733
+ id: getTranslation("userDetail.common.failed"),
734
+ defaultMessage: "Failed"
735
+ })
736
+ );
737
+ return result.data;
738
+ },
739
+ onSuccess: () => {
740
+ toggleNotification({
741
+ type: "success",
742
+ message: formatMessage({
743
+ id: getTranslation("userDetail.notifications.verificationSent"),
744
+ defaultMessage: "Verification email sent"
745
+ })
746
+ });
747
+ },
748
+ onError: (err) => {
749
+ toggleNotification({
750
+ type: "danger",
751
+ message: err.message ?? formatMessage({
752
+ id: getTranslation("userDetail.common.sendEmailFailed"),
753
+ defaultMessage: "Failed to send email"
754
+ })
755
+ });
756
+ }
757
+ });
758
+ const sendResetPasswordMutation = useMutation({
759
+ mutationFn: async () => {
760
+ if (!emailCallbackUrl) {
761
+ throw new Error(
762
+ formatMessage({
763
+ id: getTranslation("userDetail.common.callbackUrlMissing"),
764
+ defaultMessage: "Set `email_callback_url` in the better-auth-dashboard plugin config to enable this action."
765
+ })
766
+ );
767
+ }
768
+ const result = await client.dash.sendResetPasswordEmail(
769
+ { callbackUrl: emailCallbackUrl },
770
+ withContext({ userId }, getAuthHeaders())
771
+ );
772
+ if (result.error)
773
+ throw new Error(
774
+ result.error.message ?? formatMessage({
775
+ id: getTranslation("userDetail.common.failed"),
776
+ defaultMessage: "Failed"
777
+ })
778
+ );
779
+ return result.data;
780
+ },
781
+ onSuccess: () => {
782
+ toggleNotification({
783
+ type: "success",
784
+ message: formatMessage({
785
+ id: getTranslation("userDetail.notifications.resetPasswordSent"),
786
+ defaultMessage: "Password reset email sent"
787
+ })
788
+ });
789
+ },
790
+ onError: (err) => {
791
+ toggleNotification({
792
+ type: "danger",
793
+ message: err.message ?? formatMessage({
794
+ id: getTranslation("userDetail.common.sendEmailFailed"),
795
+ defaultMessage: "Failed to send email"
796
+ })
797
+ });
798
+ }
799
+ });
800
+ const enableTwoFactorMutation = useMutation({
801
+ mutationFn: async () => {
802
+ const result = await client.dash.enableTwoFactor(
803
+ {},
804
+ withContext({ userId }, getAuthHeaders())
805
+ );
806
+ if (result.error)
807
+ throw new Error(
808
+ result.error.message ?? formatMessage({
809
+ id: getTranslation("userDetail.common.enableTwoFactorFailed"),
810
+ defaultMessage: "Failed to enable 2FA"
811
+ })
812
+ );
813
+ return result.data;
814
+ },
815
+ onSuccess: (data) => {
816
+ if (data) setTwoFactorData(data);
817
+ qc.invalidateQueries({ queryKey: ["dash-user", userId] });
818
+ toggleNotification({
819
+ type: "success",
820
+ message: formatMessage({
821
+ id: getTranslation("userDetail.notifications.twoFactorEnabled"),
822
+ defaultMessage: "Two-factor authentication enabled"
823
+ })
824
+ });
825
+ },
826
+ onError: (err) => {
827
+ toggleNotification({
828
+ type: "danger",
829
+ message: err.message ?? formatMessage({
830
+ id: getTranslation("userDetail.common.enableTwoFactorFailed"),
831
+ defaultMessage: "Failed to enable 2FA"
832
+ })
833
+ });
834
+ }
835
+ });
836
+ const disableTwoFactorMutation = useMutation({
837
+ mutationFn: async () => {
838
+ const result = await client.dash.disableTwoFactor(
839
+ {},
840
+ withContext({ userId }, getAuthHeaders())
841
+ );
842
+ if (result.error)
843
+ throw new Error(
844
+ result.error.message ?? formatMessage({
845
+ id: getTranslation("userDetail.common.disableTwoFactorFailed"),
846
+ defaultMessage: "Failed to disable 2FA"
847
+ })
848
+ );
849
+ return result.data;
850
+ },
851
+ onSuccess: () => {
852
+ setConfirmDisable2FA(false);
853
+ setTwoFactorData(null);
854
+ setTotpUriData(null);
855
+ setBackupCodesData(null);
856
+ qc.invalidateQueries({ queryKey: ["dash-user", userId] });
857
+ toggleNotification({
858
+ type: "success",
859
+ message: formatMessage({
860
+ id: getTranslation("userDetail.notifications.twoFactorDisabled"),
861
+ defaultMessage: "Two-factor authentication disabled"
862
+ })
863
+ });
864
+ },
865
+ onError: (err) => {
866
+ toggleNotification({
867
+ type: "danger",
868
+ message: err.message ?? formatMessage({
869
+ id: getTranslation("userDetail.common.disableTwoFactorFailed"),
870
+ defaultMessage: "Failed to disable 2FA"
871
+ })
872
+ });
873
+ }
874
+ });
875
+ const viewTotpUriMutation = useMutation({
876
+ mutationFn: async () => {
877
+ const result = await client.dash.viewTwoFactorTotpUri(
878
+ {},
879
+ withContext({ userId }, getAuthHeaders())
880
+ );
881
+ if (result.error)
882
+ throw new Error(
883
+ result.error.message ?? formatMessage({
884
+ id: getTranslation("userDetail.common.loadTotpUriFailed"),
885
+ defaultMessage: "Failed to load TOTP URI"
886
+ })
887
+ );
888
+ return result.data;
889
+ },
890
+ onSuccess: (data) => {
891
+ if (data) {
892
+ setTotpUriData(data);
893
+ setShowTotpModal(true);
894
+ }
895
+ },
896
+ onError: (err) => {
897
+ toggleNotification({
898
+ type: "danger",
899
+ message: err.message ?? formatMessage({
900
+ id: getTranslation("userDetail.common.loadTotpUriFailed"),
901
+ defaultMessage: "Failed to load TOTP URI"
902
+ })
903
+ });
904
+ }
905
+ });
906
+ const viewBackupCodesMutation = useMutation({
907
+ mutationFn: async () => {
908
+ const result = await client.dash.viewBackupCodes(
909
+ {},
910
+ withContext({ userId }, getAuthHeaders())
911
+ );
912
+ if (result.error)
913
+ throw new Error(
914
+ result.error.message ?? formatMessage({
915
+ id: getTranslation("userDetail.common.loadBackupCodesFailed"),
916
+ defaultMessage: "Failed to load backup codes"
917
+ })
918
+ );
919
+ return result.data;
920
+ },
921
+ onSuccess: (data) => {
922
+ if (data) {
923
+ setBackupCodesData(data.backupCodes);
924
+ setShowBackupCodesModal(true);
925
+ }
926
+ },
927
+ onError: (err) => {
928
+ toggleNotification({
929
+ type: "danger",
930
+ message: err.message ?? formatMessage({
931
+ id: getTranslation("userDetail.common.loadBackupCodesFailed"),
932
+ defaultMessage: "Failed to load backup codes"
933
+ })
934
+ });
935
+ }
936
+ });
937
+ const generateBackupCodesMutation = useMutation({
938
+ mutationFn: async () => {
939
+ const result = await client.dash.generateBackupCodes(
940
+ {},
941
+ withContext({ userId }, getAuthHeaders())
942
+ );
943
+ if (result.error)
944
+ throw new Error(
945
+ result.error.message ?? formatMessage({
946
+ id: getTranslation("userDetail.common.generateBackupCodesFailed"),
947
+ defaultMessage: "Failed to generate backup codes"
948
+ })
949
+ );
950
+ return result.data;
951
+ },
952
+ onSuccess: (data) => {
953
+ setConfirmRegenerateBackupCodes(false);
954
+ if (data) {
955
+ setBackupCodesData(data.backupCodes);
956
+ setShowBackupCodesModal(true);
957
+ }
958
+ toggleNotification({
959
+ type: "success",
960
+ message: formatMessage({
961
+ id: getTranslation("userDetail.notifications.backupCodesGenerated"),
962
+ defaultMessage: "New backup codes generated"
963
+ })
964
+ });
965
+ },
966
+ onError: (err) => {
967
+ toggleNotification({
968
+ type: "danger",
969
+ message: err.message ?? formatMessage({
970
+ id: getTranslation("userDetail.common.generateBackupCodesFailed"),
971
+ defaultMessage: "Failed to generate backup codes"
972
+ })
973
+ });
974
+ }
975
+ });
976
+ const hasEdits = editName !== void 0 || editEmail !== void 0 || editEmailVerified !== void 0 || editImage !== void 0 || Object.keys(editExtra).length > 0;
977
+ const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
978
+ const banExpiryPreview = banExpiresDays && !Number.isNaN(parseInt(banExpiresDays, 10)) ? new Date(
979
+ Date.now() + parseInt(banExpiresDays, 10) * 864e5
980
+ ).toLocaleDateString(void 0, {
981
+ weekday: "long",
982
+ year: "numeric",
983
+ month: "long",
984
+ day: "numeric"
985
+ }) : null;
986
+ const drawerTitle = /* @__PURE__ */ jsxs(Flex, { tag: "span", gap: 3, alignItems: "center", children: [
987
+ /* @__PURE__ */ jsx(Avatar, { name: user?.name ?? "?", src: user?.image, size: 32 }),
988
+ /* @__PURE__ */ jsxs(Flex, { tag: "span", direction: "column", alignItems: "flex-start", children: [
989
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", textColor: "neutral800", children: user?.name ?? formatMessage({
990
+ id: getTranslation("userDetail.header.fallbackName"),
991
+ defaultMessage: "User"
992
+ }) }),
993
+ user?.email && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: user.email })
994
+ ] }),
995
+ user?.banned && /* @__PURE__ */ jsx(Badge, { variant: "danger", size: "S", children: formatMessage({
996
+ id: getTranslation("userDetail.header.banned"),
997
+ defaultMessage: "Banned"
998
+ }) })
999
+ ] });
1000
+ const profileFooter = activeTab === "profile" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1001
+ /* @__PURE__ */ jsx(
1002
+ Button,
1003
+ {
1004
+ variant: "tertiary",
1005
+ onClick: () => {
1006
+ setEditName(void 0);
1007
+ setEditEmail(void 0);
1008
+ setEditEmailVerified(void 0);
1009
+ setEditImage(void 0);
1010
+ setEditExtra({});
1011
+ onClose();
1012
+ },
1013
+ children: formatMessage({
1014
+ id: getTranslation("userDetail.footer.discard"),
1015
+ defaultMessage: "Discard"
1016
+ })
1017
+ }
1018
+ ),
1019
+ /* @__PURE__ */ jsx(
1020
+ Button,
1021
+ {
1022
+ disabled: !hasEdits,
1023
+ loading: updateMutation.isLoading,
1024
+ onClick: () => updateMutation.mutate(),
1025
+ "data-testid": "save-user-btn",
1026
+ children: formatMessage({
1027
+ id: getTranslation("userDetail.footer.save"),
1028
+ defaultMessage: "Save changes"
1029
+ })
1030
+ }
1031
+ )
1032
+ ] }) : void 0;
1033
+ return /* @__PURE__ */ jsxs(
1034
+ SheetDialog,
1035
+ {
1036
+ title: drawerTitle,
1037
+ actionButtons: profileFooter,
1038
+ onClose,
1039
+ "data-testid": "user-detail-drawer",
1040
+ children: [
1041
+ userQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: formatMessage({
1042
+ id: getTranslation("userDetail.loadingUser"),
1043
+ defaultMessage: "Loading user…"
1044
+ }) }) }) : userQuery.isError ? /* @__PURE__ */ jsx(
1045
+ Alert,
1046
+ {
1047
+ closeLabel: formatMessage({
1048
+ id: getTranslation("userDetail.common.close"),
1049
+ defaultMessage: "Close"
1050
+ }),
1051
+ title: formatMessage({
1052
+ id: getTranslation("userDetail.common.loadUserFailed"),
1053
+ defaultMessage: "Failed to load user"
1054
+ }),
1055
+ variant: "danger",
1056
+ children: userQuery.error instanceof Error ? userQuery.error.message : formatMessage({
1057
+ id: getTranslation("userDetail.errors.generic"),
1058
+ defaultMessage: "An error occurred"
1059
+ })
1060
+ }
1061
+ ) : /* @__PURE__ */ jsxs(Tabs.Root, { defaultValue: "profile", onValueChange: setActiveTab, children: [
1062
+ /* @__PURE__ */ jsxs(Tabs.List, { children: [
1063
+ /* @__PURE__ */ jsx(Tabs.Trigger, { value: "profile", children: formatMessage({
1064
+ id: getTranslation("userDetail.tabs.profile"),
1065
+ defaultMessage: "Profile"
1066
+ }) }),
1067
+ /* @__PURE__ */ jsx(Tabs.Trigger, { value: "security", children: formatMessage({
1068
+ id: getTranslation("userDetail.tabs.security"),
1069
+ defaultMessage: "Security"
1070
+ }) }),
1071
+ banEnabled && /* @__PURE__ */ jsx(Tabs.Trigger, { value: "ban", children: formatMessage({
1072
+ id: getTranslation("userDetail.tabs.ban"),
1073
+ defaultMessage: "Ban"
1074
+ }) }),
1075
+ /* @__PURE__ */ jsx(Tabs.Trigger, { value: "sessions", children: formatMessage({
1076
+ id: getTranslation("userDetail.tabs.sessions"),
1077
+ defaultMessage: "Sessions"
1078
+ }) }),
1079
+ /* @__PURE__ */ jsx(Tabs.Trigger, { value: "organizations", children: formatMessage({
1080
+ id: getTranslation("userDetail.tabs.organizations"),
1081
+ defaultMessage: "Organizations"
1082
+ }) })
1083
+ ] }),
1084
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "profile", children: /* @__PURE__ */ jsxs(EditLayout, { children: [
1085
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, children: [
1086
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1087
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1088
+ id: getTranslation("userDetail.profile.personalInfo"),
1089
+ defaultMessage: "Personal information"
1090
+ }) }),
1091
+ /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, children: [
1092
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
1093
+ id: getTranslation("userDetail.profile.fullName"),
1094
+ defaultMessage: "Full name"
1095
+ }) }),
1096
+ /* @__PURE__ */ jsx(
1097
+ TextInput,
1098
+ {
1099
+ value: displayName,
1100
+ onChange: (e) => setEditName(e.target.value),
1101
+ "data-testid": "user-name-input"
1102
+ }
1103
+ )
1104
+ ] }),
1105
+ /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, children: [
1106
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
1107
+ id: getTranslation("userDetail.profile.email"),
1108
+ defaultMessage: "Email address"
1109
+ }) }),
1110
+ /* @__PURE__ */ jsx(
1111
+ TextInput,
1112
+ {
1113
+ type: "email",
1114
+ value: displayEmail,
1115
+ onChange: (e) => setEditEmail(e.target.value)
1116
+ }
1117
+ )
1118
+ ] }),
1119
+ /* @__PURE__ */ jsx(
1120
+ Checkbox,
1121
+ {
1122
+ checked: displayEmailVerified,
1123
+ onCheckedChange: (checked) => setEditEmailVerified(checked),
1124
+ children: formatMessage({
1125
+ id: getTranslation("userDetail.profile.markVerified"),
1126
+ defaultMessage: "Mark email as verified"
1127
+ })
1128
+ }
1129
+ ),
1130
+ /* @__PURE__ */ jsx(
1131
+ MediaPickerField,
1132
+ {
1133
+ label: formatMessage({
1134
+ id: getTranslation("userDetail.profile.avatar.label"),
1135
+ defaultMessage: "Avatar URL"
1136
+ }),
1137
+ name: "image",
1138
+ value: displayImage,
1139
+ onChange: setEditImage,
1140
+ placeholder: formatMessage({
1141
+ id: getTranslation(
1142
+ "userDetail.profile.avatar.placeholder"
1143
+ ),
1144
+ defaultMessage: "https://example.com/avatar.png"
1145
+ }),
1146
+ hint: formatMessage({
1147
+ id: getTranslation("userDetail.profile.avatar.hint"),
1148
+ defaultMessage: "Optional — publicly accessible image URL"
1149
+ })
1150
+ }
1151
+ )
1152
+ ] }),
1153
+ /* @__PURE__ */ jsx(
1154
+ CustomFieldsSection,
1155
+ {
1156
+ fields: customFields,
1157
+ data: extraData,
1158
+ onChange: handleExtraChange
1159
+ }
1160
+ )
1161
+ ] }),
1162
+ /* @__PURE__ */ jsxs(EditSidebar, { children: [
1163
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1164
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1165
+ id: getTranslation("userDetail.profile.details"),
1166
+ defaultMessage: "Details"
1167
+ }) }),
1168
+ /* @__PURE__ */ jsxs(Grid.Root, { gap: 3, gridCols: 2, children: [
1169
+ /* @__PURE__ */ jsx(Grid.Item, { col: 2, children: /* @__PURE__ */ jsxs(MetaItem, { children: [
1170
+ /* @__PURE__ */ jsx(MetaKey, { children: formatMessage({
1171
+ id: getTranslation("userDetail.profile.userId"),
1172
+ defaultMessage: "User ID"
1173
+ }) }),
1174
+ /* @__PURE__ */ jsx(MonoChip, { children: user?.id })
1175
+ ] }) }),
1176
+ /* @__PURE__ */ jsx(Grid.Item, { col: 1, children: /* @__PURE__ */ jsxs(MetaItem, { children: [
1177
+ /* @__PURE__ */ jsx(MetaKey, { children: formatMessage({
1178
+ id: getTranslation("userDetail.profile.created"),
1179
+ defaultMessage: "Created"
1180
+ }) }),
1181
+ /* @__PURE__ */ jsx(MetaVal, { children: user?.createdAt ? new Date(user.createdAt).toLocaleString() : "—" })
1182
+ ] }) }),
1183
+ /* @__PURE__ */ jsx(Grid.Item, { col: 1, children: /* @__PURE__ */ jsxs(MetaItem, { children: [
1184
+ /* @__PURE__ */ jsx(MetaKey, { children: formatMessage({
1185
+ id: getTranslation(
1186
+ "userDetail.profile.lastUpdated"
1187
+ ),
1188
+ defaultMessage: "Last updated"
1189
+ }) }),
1190
+ /* @__PURE__ */ jsx(MetaVal, { children: user?.updatedAt ? new Date(user.updatedAt).toLocaleString() : "—" })
1191
+ ] }) })
1192
+ ] })
1193
+ ] }),
1194
+ emailVerificationEnabled && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
1195
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1196
+ id: getTranslation(
1197
+ "userDetail.profile.emailActions.title"
1198
+ ),
1199
+ defaultMessage: "Email actions"
1200
+ }) }),
1201
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
1202
+ /* @__PURE__ */ jsx(
1203
+ Button,
1204
+ {
1205
+ variant: "secondary",
1206
+ size: "S",
1207
+ fullWidth: true,
1208
+ loading: sendVerificationMutation.isLoading,
1209
+ disabled: !emailCallbackUrl || user?.emailVerified,
1210
+ onClick: () => sendVerificationMutation.mutate(),
1211
+ children: formatMessage({
1212
+ id: getTranslation(
1213
+ "userDetail.profile.emailActions.sendVerification"
1214
+ ),
1215
+ defaultMessage: "Send verification email"
1216
+ })
1217
+ }
1218
+ ),
1219
+ /* @__PURE__ */ jsx(
1220
+ Button,
1221
+ {
1222
+ variant: "secondary",
1223
+ size: "S",
1224
+ fullWidth: true,
1225
+ loading: sendResetPasswordMutation.isLoading,
1226
+ disabled: !emailCallbackUrl,
1227
+ onClick: () => sendResetPasswordMutation.mutate(),
1228
+ children: formatMessage({
1229
+ id: getTranslation(
1230
+ "userDetail.profile.emailActions.sendReset"
1231
+ ),
1232
+ defaultMessage: "Send password reset"
1233
+ })
1234
+ }
1235
+ ),
1236
+ !settingsQuery.isLoading && !emailCallbackUrl && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
1237
+ id: getTranslation(
1238
+ "userDetail.profile.emailActions.callbackUrlHint"
1239
+ ),
1240
+ defaultMessage: "Set `email_callback_url` in the better-auth-dashboard plugin config to enable these actions."
1241
+ }) })
1242
+ ] })
1243
+ ] }),
1244
+ /* @__PURE__ */ jsx(
1245
+ EditViewSidePanels,
1246
+ {
1247
+ model: "plugin::better-auth.user",
1248
+ documentId: strapiUserQuery.data?.documentId,
1249
+ document: strapiUserQuery.data ?? void 0
1250
+ }
1251
+ )
1252
+ ] })
1253
+ ] }) }),
1254
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "security", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
1255
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1256
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1257
+ id: getTranslation("userDetail.security.password.title"),
1258
+ defaultMessage: "Password"
1259
+ }) }),
1260
+ /* @__PURE__ */ jsxs(
1261
+ Field.Root,
1262
+ {
1263
+ hint: formatMessage({
1264
+ id: getTranslation("userDetail.security.password.hint"),
1265
+ defaultMessage: "The user will be able to sign in with this new password"
1266
+ }),
1267
+ children: [
1268
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
1269
+ id: getTranslation("userDetail.security.password.label"),
1270
+ defaultMessage: "New password"
1271
+ }) }),
1272
+ /* @__PURE__ */ jsx(
1273
+ TextInput,
1274
+ {
1275
+ type: "password",
1276
+ value: newPassword,
1277
+ onChange: (e) => setNewPassword(e.target.value),
1278
+ placeholder: formatMessage({
1279
+ id: getTranslation(
1280
+ "userDetail.security.password.placeholder"
1281
+ ),
1282
+ defaultMessage: "Enter a strong password…"
1283
+ })
1284
+ }
1285
+ ),
1286
+ /* @__PURE__ */ jsx(Field.Hint, {})
1287
+ ]
1288
+ }
1289
+ ),
1290
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
1291
+ Button,
1292
+ {
1293
+ disabled: !newPassword,
1294
+ loading: passwordMutation.isLoading,
1295
+ onClick: () => passwordMutation.mutate(),
1296
+ children: formatMessage({
1297
+ id: getTranslation("userDetail.security.password.submit"),
1298
+ defaultMessage: "Set password"
1299
+ })
1300
+ }
1301
+ ) })
1302
+ ] }),
1303
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1304
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1305
+ id: getTranslation(
1306
+ "userDetail.security.linkedAccounts.title"
1307
+ ),
1308
+ defaultMessage: "Linked accounts"
1309
+ }) }),
1310
+ user?.account && user.account.length > 0 ? /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: user.account.map((acc) => /* @__PURE__ */ jsxs(AccountRow, { children: [
1311
+ /* @__PURE__ */ jsx(ProviderBadge, { children: acc.providerId }),
1312
+ /* @__PURE__ */ jsx(
1313
+ Button,
1314
+ {
1315
+ variant: "danger-light",
1316
+ size: "S",
1317
+ onClick: () => setConfirmUnlinkAccountId(acc.providerId),
1318
+ children: formatMessage({
1319
+ id: getTranslation("userDetail.common.unlink"),
1320
+ defaultMessage: "Unlink"
1321
+ })
1322
+ }
1323
+ )
1324
+ ] }, acc.id)) }) : /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
1325
+ id: getTranslation(
1326
+ "userDetail.security.linkedAccounts.empty"
1327
+ ),
1328
+ defaultMessage: "No linked OAuth accounts."
1329
+ }) })
1330
+ ] }),
1331
+ twoFactorEnabled && /* @__PURE__ */ jsxs(FormSection, { children: [
1332
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
1333
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1334
+ id: getTranslation(
1335
+ "userDetail.security.twoFactor.title"
1336
+ ),
1337
+ defaultMessage: "Two-factor authentication"
1338
+ }) }),
1339
+ /* @__PURE__ */ jsx(
1340
+ Badge,
1341
+ {
1342
+ size: "S",
1343
+ variant: user?.twoFactorEnabled ? "success" : "neutral",
1344
+ children: user?.twoFactorEnabled ? formatMessage({
1345
+ id: getTranslation(
1346
+ "userDetail.security.twoFactor.status.enabled"
1347
+ ),
1348
+ defaultMessage: "Enabled"
1349
+ }) : formatMessage({
1350
+ id: getTranslation(
1351
+ "userDetail.security.twoFactor.status.disabled"
1352
+ ),
1353
+ defaultMessage: "Not enabled"
1354
+ })
1355
+ }
1356
+ )
1357
+ ] }),
1358
+ user?.twoFactorEnabled ? /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, children: [
1359
+ /* @__PURE__ */ jsxs(AccountRow, { children: [
1360
+ /* @__PURE__ */ jsxs(
1361
+ Flex,
1362
+ {
1363
+ direction: "column",
1364
+ gap: 1,
1365
+ alignItems: "flex-start",
1366
+ children: [
1367
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: formatMessage({
1368
+ id: getTranslation(
1369
+ "userDetail.common.totpAuthenticator"
1370
+ ),
1371
+ defaultMessage: "TOTP authenticator"
1372
+ }) }),
1373
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
1374
+ id: getTranslation(
1375
+ "userDetail.security.twoFactor.totp.description"
1376
+ ),
1377
+ defaultMessage: "View the URI and secret for this user's authenticator app."
1378
+ }) })
1379
+ ]
1380
+ }
1381
+ ),
1382
+ /* @__PURE__ */ jsx(
1383
+ Button,
1384
+ {
1385
+ variant: "secondary",
1386
+ size: "S",
1387
+ loading: viewTotpUriMutation.isLoading,
1388
+ onClick: () => viewTotpUriMutation.mutate(),
1389
+ children: totpUriData ? formatMessage({
1390
+ id: getTranslation("userDetail.common.refresh"),
1391
+ defaultMessage: "Refresh"
1392
+ }) : formatMessage({
1393
+ id: getTranslation("userDetail.common.view"),
1394
+ defaultMessage: "View"
1395
+ })
1396
+ }
1397
+ )
1398
+ ] }),
1399
+ /* @__PURE__ */ jsxs(AccountRow, { children: [
1400
+ /* @__PURE__ */ jsxs(
1401
+ Flex,
1402
+ {
1403
+ direction: "column",
1404
+ gap: 1,
1405
+ alignItems: "flex-start",
1406
+ children: [
1407
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: formatMessage({
1408
+ id: getTranslation(
1409
+ "userDetail.common.backupCodes"
1410
+ ),
1411
+ defaultMessage: "Backup codes"
1412
+ }) }),
1413
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
1414
+ id: getTranslation(
1415
+ "userDetail.security.twoFactor.backupCodes.description"
1416
+ ),
1417
+ defaultMessage: "View or regenerate the user's recovery codes."
1418
+ }) })
1419
+ ]
1420
+ }
1421
+ ),
1422
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1423
+ /* @__PURE__ */ jsx(
1424
+ Button,
1425
+ {
1426
+ variant: "secondary",
1427
+ size: "S",
1428
+ loading: viewBackupCodesMutation.isLoading,
1429
+ onClick: () => viewBackupCodesMutation.mutate(),
1430
+ children: backupCodesData ? formatMessage({
1431
+ id: getTranslation(
1432
+ "userDetail.common.refresh"
1433
+ ),
1434
+ defaultMessage: "Refresh"
1435
+ }) : formatMessage({
1436
+ id: getTranslation("userDetail.common.view"),
1437
+ defaultMessage: "View"
1438
+ })
1439
+ }
1440
+ ),
1441
+ /* @__PURE__ */ jsx(
1442
+ Button,
1443
+ {
1444
+ variant: "secondary",
1445
+ size: "S",
1446
+ onClick: () => setConfirmRegenerateBackupCodes(true),
1447
+ children: formatMessage({
1448
+ id: getTranslation(
1449
+ "userDetail.common.regenerate"
1450
+ ),
1451
+ defaultMessage: "Regenerate"
1452
+ })
1453
+ }
1454
+ )
1455
+ ] })
1456
+ ] }),
1457
+ /* @__PURE__ */ jsxs(DangerRow, { children: [
1458
+ /* @__PURE__ */ jsxs(
1459
+ Flex,
1460
+ {
1461
+ direction: "column",
1462
+ gap: 1,
1463
+ alignItems: "flex-start",
1464
+ children: [
1465
+ /* @__PURE__ */ jsx(
1466
+ Typography,
1467
+ {
1468
+ variant: "omega",
1469
+ fontWeight: "semiBold",
1470
+ textColor: "danger600",
1471
+ children: formatMessage({
1472
+ id: getTranslation(
1473
+ "userDetail.common.disableTwoFactorTitle"
1474
+ ),
1475
+ defaultMessage: "Disable two-factor authentication"
1476
+ })
1477
+ }
1478
+ ),
1479
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger500", children: formatMessage({
1480
+ id: getTranslation(
1481
+ "userDetail.security.twoFactor.disable.description"
1482
+ ),
1483
+ defaultMessage: "The user will no longer need a second factor to sign in."
1484
+ }) })
1485
+ ]
1486
+ }
1487
+ ),
1488
+ /* @__PURE__ */ jsx(
1489
+ Button,
1490
+ {
1491
+ variant: "danger-light",
1492
+ size: "S",
1493
+ onClick: () => setConfirmDisable2FA(true),
1494
+ children: formatMessage({
1495
+ id: getTranslation(
1496
+ "userDetail.common.disableTwoFactor"
1497
+ ),
1498
+ defaultMessage: "Disable 2FA"
1499
+ })
1500
+ }
1501
+ )
1502
+ ] })
1503
+ ] }) : /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, children: [
1504
+ twoFactorData && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
1505
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
1506
+ id: getTranslation(
1507
+ "userDetail.security.twoFactor.setup.instructions"
1508
+ ),
1509
+ defaultMessage: "Scan the TOTP URI with an authenticator app, or enter the secret key manually."
1510
+ }) }),
1511
+ /* @__PURE__ */ jsx(
1512
+ ReadOnlyField,
1513
+ {
1514
+ label: formatMessage({
1515
+ id: getTranslation("userDetail.common.totpUri"),
1516
+ defaultMessage: "TOTP URI"
1517
+ }),
1518
+ value: twoFactorData.totpURI
1519
+ }
1520
+ ),
1521
+ /* @__PURE__ */ jsx(
1522
+ ReadOnlyField,
1523
+ {
1524
+ label: formatMessage({
1525
+ id: getTranslation("userDetail.common.secretKey"),
1526
+ defaultMessage: "Secret key"
1527
+ }),
1528
+ value: twoFactorData.secret
1529
+ }
1530
+ ),
1531
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
1532
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1533
+ id: getTranslation(
1534
+ "userDetail.common.backupCodes"
1535
+ ),
1536
+ defaultMessage: "Backup codes"
1537
+ }) }),
1538
+ /* @__PURE__ */ jsx(BackupCodes, { codes: twoFactorData.backupCodes })
1539
+ ] })
1540
+ ] }),
1541
+ /* @__PURE__ */ jsxs(AccountRow, { children: [
1542
+ /* @__PURE__ */ jsxs(
1543
+ Flex,
1544
+ {
1545
+ direction: "column",
1546
+ gap: 1,
1547
+ alignItems: "flex-start",
1548
+ children: [
1549
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: formatMessage({
1550
+ id: getTranslation(
1551
+ "userDetail.security.twoFactor.enable.title"
1552
+ ),
1553
+ defaultMessage: "Enable two-factor authentication"
1554
+ }) }),
1555
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
1556
+ id: getTranslation(
1557
+ "userDetail.security.twoFactor.enable.description"
1558
+ ),
1559
+ defaultMessage: "Add an extra layer of security to this account."
1560
+ }) })
1561
+ ]
1562
+ }
1563
+ ),
1564
+ /* @__PURE__ */ jsx(
1565
+ Button,
1566
+ {
1567
+ size: "S",
1568
+ loading: enableTwoFactorMutation.isLoading,
1569
+ onClick: () => enableTwoFactorMutation.mutate(),
1570
+ children: formatMessage({
1571
+ id: getTranslation(
1572
+ "userDetail.security.twoFactor.enable.submit"
1573
+ ),
1574
+ defaultMessage: "Enable 2FA"
1575
+ })
1576
+ }
1577
+ )
1578
+ ] })
1579
+ ] })
1580
+ ] })
1581
+ ] }) }),
1582
+ banEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxs(FormSection, { children: [
1583
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1584
+ id: getTranslation("userDetail.ban.status.title"),
1585
+ defaultMessage: "Ban status"
1586
+ }) }),
1587
+ /* @__PURE__ */ jsxs(WarnCard, { children: [
1588
+ /* @__PURE__ */ jsx(
1589
+ Typography,
1590
+ {
1591
+ variant: "omega",
1592
+ fontWeight: "semiBold",
1593
+ textColor: "danger600",
1594
+ children: formatMessage({
1595
+ id: getTranslation("userDetail.ban.status.current"),
1596
+ defaultMessage: "This user is currently banned"
1597
+ })
1598
+ }
1599
+ ),
1600
+ user.banReason && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: formatMessage(
1601
+ {
1602
+ id: getTranslation(
1603
+ "userDetail.ban.status.reason"
1604
+ ),
1605
+ defaultMessage: "Reason: {reason}"
1606
+ },
1607
+ { reason: user.banReason }
1608
+ ) }),
1609
+ user.banExpires ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: formatMessage(
1610
+ {
1611
+ id: getTranslation(
1612
+ "userDetail.ban.status.expires"
1613
+ ),
1614
+ defaultMessage: "Expires: {date}"
1615
+ },
1616
+ {
1617
+ date: new Date(
1618
+ user.banExpires
1619
+ ).toLocaleDateString(void 0, {
1620
+ weekday: "long",
1621
+ year: "numeric",
1622
+ month: "long",
1623
+ day: "numeric"
1624
+ })
1625
+ }
1626
+ ) }) : /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: formatMessage({
1627
+ id: getTranslation(
1628
+ "userDetail.ban.status.permanent"
1629
+ ),
1630
+ defaultMessage: "Duration: Permanent"
1631
+ }) })
1632
+ ] }),
1633
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
1634
+ Button,
1635
+ {
1636
+ variant: "secondary",
1637
+ onClick: () => setConfirmUnban(true),
1638
+ children: formatMessage({
1639
+ id: getTranslation("userDetail.common.liftBan"),
1640
+ defaultMessage: "Lift ban"
1641
+ })
1642
+ }
1643
+ ) })
1644
+ ] }) : /* @__PURE__ */ jsxs(FormSection, { children: [
1645
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1646
+ id: getTranslation("userDetail.ban.apply.title"),
1647
+ defaultMessage: "Apply ban"
1648
+ }) }),
1649
+ /* @__PURE__ */ jsxs(
1650
+ Field.Root,
1651
+ {
1652
+ hint: formatMessage({
1653
+ id: getTranslation("userDetail.ban.apply.reason.hint"),
1654
+ defaultMessage: "Optional — will be shown to the user"
1655
+ }),
1656
+ style: { width: "100%" },
1657
+ children: [
1658
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
1659
+ id: getTranslation(
1660
+ "userDetail.ban.apply.reason.label"
1661
+ ),
1662
+ defaultMessage: "Reason"
1663
+ }) }),
1664
+ /* @__PURE__ */ jsx(
1665
+ TextInput,
1666
+ {
1667
+ value: banReason,
1668
+ onChange: (e) => setBanReason(e.target.value),
1669
+ placeholder: formatMessage({
1670
+ id: getTranslation(
1671
+ "userDetail.ban.apply.reason.placeholder"
1672
+ ),
1673
+ defaultMessage: "e.g. Violated terms of service"
1674
+ })
1675
+ }
1676
+ ),
1677
+ /* @__PURE__ */ jsx(Field.Hint, {})
1678
+ ]
1679
+ }
1680
+ ),
1681
+ /* @__PURE__ */ jsxs(
1682
+ Field.Root,
1683
+ {
1684
+ hint: formatMessage({
1685
+ id: getTranslation(
1686
+ "userDetail.ban.apply.duration.hint"
1687
+ ),
1688
+ defaultMessage: "Leave empty for a permanent ban"
1689
+ }),
1690
+ style: { width: "100%" },
1691
+ children: [
1692
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
1693
+ id: getTranslation(
1694
+ "userDetail.ban.apply.duration.label"
1695
+ ),
1696
+ defaultMessage: "Duration (days)"
1697
+ }) }),
1698
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "center", children: [
1699
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
1700
+ NumberInput,
1701
+ {
1702
+ value: banExpiresDays ? Number(banExpiresDays) : void 0,
1703
+ onValueChange: (v) => setBanExpiresDays(
1704
+ v === void 0 ? "" : String(v)
1705
+ ),
1706
+ placeholder: formatMessage({
1707
+ id: getTranslation(
1708
+ "userDetail.ban.apply.duration.placeholder"
1709
+ ),
1710
+ defaultMessage: "e.g. 30"
1711
+ })
1712
+ }
1713
+ ) }),
1714
+ /* @__PURE__ */ jsx(Box, { shrink: 0, children: /* @__PURE__ */ jsx(PreviewPill, { children: banExpiryPreview ? formatMessage(
1715
+ {
1716
+ id: getTranslation(
1717
+ "userDetail.ban.apply.preview.expires"
1718
+ ),
1719
+ defaultMessage: "Expires {date}"
1720
+ },
1721
+ { date: banExpiryPreview }
1722
+ ) : formatMessage({
1723
+ id: getTranslation(
1724
+ "userDetail.ban.apply.preview.permanent"
1725
+ ),
1726
+ defaultMessage: "Permanent ban"
1727
+ }) }) })
1728
+ ] }),
1729
+ /* @__PURE__ */ jsx(Field.Hint, {})
1730
+ ]
1731
+ }
1732
+ ),
1733
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
1734
+ Button,
1735
+ {
1736
+ variant: "danger",
1737
+ onClick: () => setConfirmBan(true),
1738
+ children: formatMessage({
1739
+ id: getTranslation("userDetail.common.banUser"),
1740
+ defaultMessage: "Ban user"
1741
+ })
1742
+ }
1743
+ ) })
1744
+ ] }) }) }),
1745
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
1746
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1747
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1748
+ id: getTranslation("userDetail.sessions.management"),
1749
+ defaultMessage: "Session management"
1750
+ }) }),
1751
+ /* @__PURE__ */ jsxs(AccountRow, { children: [
1752
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
1753
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: formatMessage({
1754
+ id: getTranslation(
1755
+ "userDetail.common.revokeAllSessions"
1756
+ ),
1757
+ defaultMessage: "Revoke all sessions"
1758
+ }) }),
1759
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
1760
+ id: getTranslation(
1761
+ "userDetail.sessions.revokeAll.description"
1762
+ ),
1763
+ defaultMessage: "Signs the user out immediately on all devices."
1764
+ }) })
1765
+ ] }),
1766
+ /* @__PURE__ */ jsx(
1767
+ Button,
1768
+ {
1769
+ variant: "danger-light",
1770
+ size: "S",
1771
+ onClick: () => setConfirmRevokeAll(true),
1772
+ style: { flexShrink: 0 },
1773
+ children: formatMessage({
1774
+ id: getTranslation("userDetail.common.revokeAll"),
1775
+ defaultMessage: "Revoke all"
1776
+ })
1777
+ }
1778
+ )
1779
+ ] })
1780
+ ] }),
1781
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1782
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1783
+ id: getTranslation("userDetail.sessions.active"),
1784
+ defaultMessage: "Active sessions"
1785
+ }) }),
1786
+ sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: formatMessage({
1787
+ id: getTranslation("userDetail.sessions.loading"),
1788
+ defaultMessage: "Loading sessions…"
1789
+ }) }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
1790
+ id: getTranslation("userDetail.sessions.empty"),
1791
+ defaultMessage: "No active sessions."
1792
+ }) }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxs(
1793
+ AccountRow,
1794
+ {
1795
+ alignItems: "flex-start",
1796
+ children: [
1797
+ /* @__PURE__ */ jsxs(
1798
+ Flex,
1799
+ {
1800
+ direction: "column",
1801
+ alignItems: "flex-start",
1802
+ gap: 1,
1803
+ flex: 1,
1804
+ minWidth: 0,
1805
+ children: [
1806
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
1807
+ session.ipAddress && /* @__PURE__ */ jsx(MonoChip, { children: session.ipAddress }),
1808
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage(
1809
+ {
1810
+ id: getTranslation(
1811
+ "userDetail.sessions.timestamps"
1812
+ ),
1813
+ defaultMessage: "Created {createdAt} · Expires {expiresAt}"
1814
+ },
1815
+ {
1816
+ createdAt: new Date(
1817
+ session.createdAt
1818
+ ).toLocaleString(),
1819
+ expiresAt: new Date(
1820
+ session.expiresAt
1821
+ ).toLocaleString()
1822
+ }
1823
+ ) })
1824
+ ] }),
1825
+ session.userAgent && /* @__PURE__ */ jsx(
1826
+ Typography,
1827
+ {
1828
+ variant: "pi",
1829
+ textColor: "neutral500",
1830
+ ellipsis: true,
1831
+ width: "100%",
1832
+ children: session.userAgent
1833
+ }
1834
+ )
1835
+ ]
1836
+ }
1837
+ ),
1838
+ /* @__PURE__ */ jsx(
1839
+ IconButton,
1840
+ {
1841
+ label: formatMessage({
1842
+ id: getTranslation(
1843
+ "userDetail.common.revokeSession"
1844
+ ),
1845
+ defaultMessage: "Revoke session"
1846
+ }),
1847
+ onClick: () => setConfirmRevokeSessionId(String(session.id)),
1848
+ children: /* @__PURE__ */ jsx(Trash, {})
1849
+ }
1850
+ )
1851
+ ]
1852
+ },
1853
+ session.documentId
1854
+ )) })
1855
+ ] })
1856
+ ] }) }),
1857
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxs(FormSection, { children: [
1858
+ /* @__PURE__ */ jsx(SectionLabel, { children: formatMessage({
1859
+ id: getTranslation("userDetail.organizations.memberships"),
1860
+ defaultMessage: "Memberships"
1861
+ }) }),
1862
+ orgsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { small: true, children: formatMessage({
1863
+ id: getTranslation("userDetail.organizations.loading"),
1864
+ defaultMessage: "Loading…"
1865
+ }) }) }) : (orgsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
1866
+ id: getTranslation("userDetail.organizations.empty"),
1867
+ defaultMessage: "Not a member of any organizations."
1868
+ }) }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: (orgsQuery.data ?? []).map(
1869
+ (org) => org ? /* @__PURE__ */ jsx(AccountRow, { children: /* @__PURE__ */ jsxs(
1870
+ Flex,
1871
+ {
1872
+ direction: "column",
1873
+ gap: 1,
1874
+ alignItems: "flex-start",
1875
+ children: [
1876
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: org.name }),
1877
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1878
+ /* @__PURE__ */ jsx(RoleBadge, { memberRole: org.role, children: org.role }),
1879
+ org.teams.length > 0 && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage(
1880
+ {
1881
+ id: getTranslation(
1882
+ "userDetail.organizations.teams"
1883
+ ),
1884
+ defaultMessage: "{count, plural, one {# team} other {# teams}}"
1885
+ },
1886
+ { count: org.teams.length }
1887
+ ) })
1888
+ ] })
1889
+ ]
1890
+ }
1891
+ ) }, org.id) : null
1892
+ ) })
1893
+ ] }) }) })
1894
+ ] }),
1895
+ confirmRevokeAll && /* @__PURE__ */ jsx(
1896
+ ConfirmDialog,
1897
+ {
1898
+ title: formatMessage({
1899
+ id: getTranslation("userDetail.common.revokeAllSessions"),
1900
+ defaultMessage: "Revoke all sessions"
1901
+ }),
1902
+ message: formatMessage({
1903
+ id: getTranslation("userDetail.confirm.revokeAll.message"),
1904
+ defaultMessage: "Are you sure you want to revoke all sessions for this user? They will be signed out on all devices."
1905
+ }),
1906
+ confirmLabel: formatMessage({
1907
+ id: getTranslation("userDetail.common.revokeAll"),
1908
+ defaultMessage: "Revoke all"
1909
+ }),
1910
+ loading: revokeAllMutation.isLoading,
1911
+ onConfirm: () => revokeAllMutation.mutate(),
1912
+ onCancel: () => setConfirmRevokeAll(false)
1913
+ }
1914
+ ),
1915
+ confirmRevokeSessionId && /* @__PURE__ */ jsx(
1916
+ ConfirmDialog,
1917
+ {
1918
+ title: formatMessage({
1919
+ id: getTranslation("userDetail.common.revokeSession"),
1920
+ defaultMessage: "Revoke session"
1921
+ }),
1922
+ message: formatMessage({
1923
+ id: getTranslation("userDetail.confirm.revokeSession.message"),
1924
+ defaultMessage: "Are you sure you want to revoke this session? The user will be signed out on this device."
1925
+ }),
1926
+ confirmLabel: formatMessage({
1927
+ id: getTranslation("userDetail.confirm.revokeSession.confirm"),
1928
+ defaultMessage: "Revoke"
1929
+ }),
1930
+ loading: revokeSessionMutation.isLoading,
1931
+ onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
1932
+ onCancel: () => setConfirmRevokeSessionId(null)
1933
+ }
1934
+ ),
1935
+ confirmBan && /* @__PURE__ */ jsx(
1936
+ ConfirmDialog,
1937
+ {
1938
+ title: formatMessage({
1939
+ id: getTranslation("userDetail.common.banUser"),
1940
+ defaultMessage: "Ban user"
1941
+ }),
1942
+ message: banReason ? formatMessage(
1943
+ {
1944
+ id: getTranslation(
1945
+ "userDetail.confirm.ban.messageWithReason"
1946
+ ),
1947
+ defaultMessage: 'Ban this user for the following reason: "{reason}"? They will be prevented from signing in.'
1948
+ },
1949
+ { reason: banReason }
1950
+ ) : formatMessage({
1951
+ id: getTranslation("userDetail.confirm.ban.message"),
1952
+ defaultMessage: "Are you sure you want to ban this user? They will be prevented from signing in."
1953
+ }),
1954
+ confirmLabel: formatMessage({
1955
+ id: getTranslation("userDetail.common.banUser"),
1956
+ defaultMessage: "Ban user"
1957
+ }),
1958
+ variant: "danger",
1959
+ loading: banMutation.isLoading,
1960
+ onConfirm: () => banMutation.mutate(),
1961
+ onCancel: () => setConfirmBan(false)
1962
+ }
1963
+ ),
1964
+ confirmUnban && /* @__PURE__ */ jsx(
1965
+ ConfirmDialog,
1966
+ {
1967
+ title: formatMessage({
1968
+ id: getTranslation("userDetail.common.liftBan"),
1969
+ defaultMessage: "Lift ban"
1970
+ }),
1971
+ message: formatMessage({
1972
+ id: getTranslation("userDetail.confirm.unban.message"),
1973
+ defaultMessage: "Are you sure you want to lift the ban on this user? They will be able to sign in again."
1974
+ }),
1975
+ confirmLabel: formatMessage({
1976
+ id: getTranslation("userDetail.common.liftBan"),
1977
+ defaultMessage: "Lift ban"
1978
+ }),
1979
+ variant: "default",
1980
+ loading: unbanMutation.isLoading,
1981
+ onConfirm: () => unbanMutation.mutate(),
1982
+ onCancel: () => setConfirmUnban(false)
1983
+ }
1984
+ ),
1985
+ confirmUnlinkAccountId && /* @__PURE__ */ jsx(
1986
+ ConfirmDialog,
1987
+ {
1988
+ title: formatMessage({
1989
+ id: getTranslation("userDetail.confirm.unlink.title"),
1990
+ defaultMessage: "Unlink account"
1991
+ }),
1992
+ message: formatMessage(
1993
+ {
1994
+ id: getTranslation("userDetail.confirm.unlink.message"),
1995
+ defaultMessage: 'Are you sure you want to unlink the "{provider}" provider from this user?'
1996
+ },
1997
+ { provider: confirmUnlinkAccountId }
1998
+ ),
1999
+ confirmLabel: formatMessage({
2000
+ id: getTranslation("userDetail.common.unlink"),
2001
+ defaultMessage: "Unlink"
2002
+ }),
2003
+ loading: unlinkAccountMutation.isLoading,
2004
+ onConfirm: () => unlinkAccountMutation.mutate(confirmUnlinkAccountId),
2005
+ onCancel: () => setConfirmUnlinkAccountId(null)
2006
+ }
2007
+ ),
2008
+ confirmDisable2FA && /* @__PURE__ */ jsx(
2009
+ ConfirmDialog,
2010
+ {
2011
+ title: formatMessage({
2012
+ id: getTranslation("userDetail.common.disableTwoFactorTitle"),
2013
+ defaultMessage: "Disable two-factor authentication"
2014
+ }),
2015
+ message: formatMessage({
2016
+ id: getTranslation("userDetail.confirm.disableTwoFactor.message"),
2017
+ defaultMessage: "Are you sure you want to disable 2FA for this user? They will no longer need a second factor to sign in."
2018
+ }),
2019
+ confirmLabel: formatMessage({
2020
+ id: getTranslation("userDetail.common.disableTwoFactor"),
2021
+ defaultMessage: "Disable 2FA"
2022
+ }),
2023
+ loading: disableTwoFactorMutation.isLoading,
2024
+ onConfirm: () => disableTwoFactorMutation.mutate(),
2025
+ onCancel: () => setConfirmDisable2FA(false)
2026
+ }
2027
+ ),
2028
+ confirmRegenerateBackupCodes && /* @__PURE__ */ jsx(
2029
+ ConfirmDialog,
2030
+ {
2031
+ title: formatMessage({
2032
+ id: getTranslation(
2033
+ "userDetail.confirm.regenerateBackupCodes.title"
2034
+ ),
2035
+ defaultMessage: "Regenerate backup codes"
2036
+ }),
2037
+ message: formatMessage({
2038
+ id: getTranslation(
2039
+ "userDetail.confirm.regenerateBackupCodes.message"
2040
+ ),
2041
+ defaultMessage: "This will invalidate all existing backup codes. New codes will be shown once after generation. Are you sure?"
2042
+ }),
2043
+ confirmLabel: formatMessage({
2044
+ id: getTranslation("userDetail.common.regenerate"),
2045
+ defaultMessage: "Regenerate"
2046
+ }),
2047
+ loading: generateBackupCodesMutation.isLoading,
2048
+ onConfirm: () => generateBackupCodesMutation.mutate(),
2049
+ onCancel: () => setConfirmRegenerateBackupCodes(false)
2050
+ }
2051
+ ),
2052
+ showTotpModal && totpUriData && /* @__PURE__ */ jsx(
2053
+ Modal.Root,
2054
+ {
2055
+ defaultOpen: true,
2056
+ onOpenChange: (open) => !open && setShowTotpModal(false),
2057
+ children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
2058
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Modal.Title, { children: formatMessage({
2059
+ id: getTranslation("userDetail.common.totpAuthenticator"),
2060
+ defaultMessage: "TOTP authenticator"
2061
+ }) }) }),
2062
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
2063
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: formatMessage({
2064
+ id: getTranslation("userDetail.totpModal.description"),
2065
+ defaultMessage: "Share these details with the user to set up their authenticator app."
2066
+ }) }),
2067
+ /* @__PURE__ */ jsx(
2068
+ ReadOnlyField,
2069
+ {
2070
+ label: formatMessage({
2071
+ id: getTranslation("userDetail.common.totpUri"),
2072
+ defaultMessage: "TOTP URI"
2073
+ }),
2074
+ value: totpUriData.totpURI
2075
+ }
2076
+ ),
2077
+ /* @__PURE__ */ jsx(
2078
+ ReadOnlyField,
2079
+ {
2080
+ label: formatMessage({
2081
+ id: getTranslation("userDetail.common.secretKey"),
2082
+ defaultMessage: "Secret key"
2083
+ }),
2084
+ value: totpUriData.secret
2085
+ }
2086
+ )
2087
+ ] }) }),
2088
+ /* @__PURE__ */ jsx(Modal.Footer, { children: /* @__PURE__ */ jsx(
2089
+ Button,
2090
+ {
2091
+ variant: "tertiary",
2092
+ onClick: () => setShowTotpModal(false),
2093
+ children: formatMessage({
2094
+ id: getTranslation("userDetail.common.close"),
2095
+ defaultMessage: "Close"
2096
+ })
2097
+ }
2098
+ ) })
2099
+ ] })
2100
+ }
2101
+ ),
2102
+ showBackupCodesModal && backupCodesData && /* @__PURE__ */ jsx(
2103
+ Modal.Root,
2104
+ {
2105
+ defaultOpen: true,
2106
+ onOpenChange: (open) => !open && setShowBackupCodesModal(false),
2107
+ children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
2108
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Modal.Title, { children: formatMessage({
2109
+ id: getTranslation("userDetail.common.backupCodes"),
2110
+ defaultMessage: "Backup codes"
2111
+ }) }) }),
2112
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
2113
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: formatMessage({
2114
+ id: getTranslation(
2115
+ "userDetail.backupCodesModal.description"
2116
+ ),
2117
+ defaultMessage: "Each code can only be used once. Store them somewhere safe."
2118
+ }) }),
2119
+ /* @__PURE__ */ jsx(BackupCodes, { codes: backupCodesData })
2120
+ ] }) }),
2121
+ /* @__PURE__ */ jsx(Modal.Footer, { children: /* @__PURE__ */ jsx(
2122
+ Button,
2123
+ {
2124
+ variant: "tertiary",
2125
+ onClick: () => setShowBackupCodesModal(false),
2126
+ children: formatMessage({
2127
+ id: getTranslation("userDetail.common.close"),
2128
+ defaultMessage: "Close"
2129
+ })
2130
+ }
2131
+ ) })
2132
+ ] })
2133
+ }
2134
+ )
2135
+ ]
2136
+ }
2137
+ );
2138
+ }
2139
+ const PAGE_SIZE = 25;
2140
+ function UsersPage({ config }) {
2141
+ const { formatMessage } = useIntl();
2142
+ const rbac = useRBAC(PERMISSIONS.user);
2143
+ const qc = useQueryClient();
2144
+ const { toggleNotification } = useNotification();
2145
+ const banEnabled = hasPlugin(config, "admin");
2146
+ const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
2147
+ const twoFactorEnabled = hasPlugin(config, "two-factor");
2148
+ const [page, setPage] = useState(1);
2149
+ const [search, setSearch] = useState("");
2150
+ const [searchInput, setSearchInput] = useState("");
2151
+ const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
2152
+ const [showCreate, setShowCreate] = useState(false);
2153
+ const [detailUserId, setDetailUserId] = useState(null);
2154
+ const [confirmDelete, setConfirmDelete] = useState(null);
2155
+ const [confirmDeleteMany, setConfirmDeleteMany] = useState(false);
2156
+ const [confirmBanMany, setConfirmBanMany] = useState(false);
2157
+ const offset = (page - 1) * PAGE_SIZE;
2158
+ const { data, isLoading, isError, error } = useUsers({
2159
+ limit: PAGE_SIZE,
2160
+ offset,
2161
+ search: search || void 0
2162
+ });
2163
+ const users = data?.users ?? [];
2164
+ const total = data?.total ?? 0;
2165
+ const pageCount = Math.ceil(total / PAGE_SIZE);
2166
+ const deleteMutation = useMutation({
2167
+ mutationFn: async (userId) => {
2168
+ const result = await client.dash.deleteUser(
2169
+ {},
2170
+ withContext({ userId }, getAuthHeaders())
2171
+ );
2172
+ if (result.error)
2173
+ throw new Error(
2174
+ result.error.message ?? formatMessage({
2175
+ id: getTranslation("users.list.errors.deleteFailed"),
2176
+ defaultMessage: "Delete failed"
2177
+ })
2178
+ );
2179
+ },
2180
+ onSuccess: () => {
2181
+ setConfirmDelete(null);
2182
+ qc.invalidateQueries({ queryKey: ["dash-users"] });
2183
+ qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
2184
+ toggleNotification({
2185
+ type: "success",
2186
+ message: formatMessage({
2187
+ id: getTranslation("users.list.notifications.deleted"),
2188
+ defaultMessage: "User deleted"
2189
+ })
2190
+ });
2191
+ },
2192
+ onError: (err) => {
2193
+ toggleNotification({
2194
+ type: "danger",
2195
+ message: err.message ?? formatMessage({
2196
+ id: getTranslation("users.list.notifications.deleteError"),
2197
+ defaultMessage: "Failed to delete user"
2198
+ })
2199
+ });
2200
+ }
2201
+ });
2202
+ const deleteManyMutation = useMutation({
2203
+ mutationFn: async (userIds) => {
2204
+ const result = await client.dash.deleteManyUsers(
2205
+ {},
2206
+ withContext({ userIds }, getAuthHeaders())
2207
+ );
2208
+ if (result.error)
2209
+ throw new Error(
2210
+ result.error.message ?? formatMessage({
2211
+ id: getTranslation("users.list.errors.deleteFailed"),
2212
+ defaultMessage: "Delete failed"
2213
+ })
2214
+ );
2215
+ return result.data;
2216
+ },
2217
+ onSuccess: (_data, userIds) => {
2218
+ setConfirmDeleteMany(false);
2219
+ setSelected(/* @__PURE__ */ new Set());
2220
+ qc.invalidateQueries({ queryKey: ["dash-users"] });
2221
+ qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
2222
+ toggleNotification({
2223
+ type: "success",
2224
+ message: formatMessage(
2225
+ {
2226
+ id: getTranslation("users.list.notifications.deletedMany"),
2227
+ defaultMessage: "{count, plural, one {# user} other {# users}} deleted"
2228
+ },
2229
+ { count: userIds.length }
2230
+ )
2231
+ });
2232
+ },
2233
+ onError: (err) => {
2234
+ toggleNotification({
2235
+ type: "danger",
2236
+ message: err.message ?? formatMessage({
2237
+ id: getTranslation("users.list.notifications.deleteManyError"),
2238
+ defaultMessage: "Failed to delete users"
2239
+ })
2240
+ });
2241
+ }
2242
+ });
2243
+ const banManyMutation = useMutation({
2244
+ mutationFn: async (userIds) => {
2245
+ const result = await client.dash.banManyUsers(
2246
+ {},
2247
+ withContext({ userIds }, getAuthHeaders())
2248
+ );
2249
+ if (result.error)
2250
+ throw new Error(
2251
+ result.error.message ?? formatMessage({
2252
+ id: getTranslation("users.list.errors.banFailed"),
2253
+ defaultMessage: "Ban failed"
2254
+ })
2255
+ );
2256
+ return result.data;
2257
+ },
2258
+ onSuccess: (_data, userIds) => {
2259
+ setConfirmBanMany(false);
2260
+ setSelected(/* @__PURE__ */ new Set());
2261
+ qc.invalidateQueries({ queryKey: ["dash-users"] });
2262
+ toggleNotification({
2263
+ type: "success",
2264
+ message: formatMessage(
2265
+ {
2266
+ id: getTranslation("users.list.notifications.bannedMany"),
2267
+ defaultMessage: "{count, plural, one {# user} other {# users}} banned"
2268
+ },
2269
+ { count: userIds.length }
2270
+ )
2271
+ });
2272
+ },
2273
+ onError: (err) => {
2274
+ toggleNotification({
2275
+ type: "danger",
2276
+ message: err.message ?? formatMessage({
2277
+ id: getTranslation("users.list.notifications.banManyError"),
2278
+ defaultMessage: "Failed to ban users"
2279
+ })
2280
+ });
2281
+ }
2282
+ });
2283
+ const toggleSelect = (id) => {
2284
+ setSelected((prev) => {
2285
+ const next = new Set(prev);
2286
+ if (next.has(id)) next.delete(id);
2287
+ else next.add(id);
2288
+ return next;
2289
+ });
2290
+ };
2291
+ const allSelected = users.length > 0 && users.every((u) => selected.has(u.id));
2292
+ const someSelected = selected.size > 0;
2293
+ const handleSelectAll = () => {
2294
+ if (allSelected) {
2295
+ setSelected(/* @__PURE__ */ new Set());
2296
+ } else {
2297
+ setSelected(new Set(users.map((u) => u.id)));
2298
+ }
2299
+ };
2300
+ const handleSearch = (e) => {
2301
+ e.preventDefault();
2302
+ setSearch(searchInput);
2303
+ setPage(1);
2304
+ };
2305
+ const canSelect = rbac.allowedActions.canDelete || rbac.allowedActions.canUpdate;
2306
+ const colCount = canSelect ? 6 : 5;
2307
+ return /* @__PURE__ */ jsxs(Main, { "data-testid": "users-page", children: [
2308
+ /* @__PURE__ */ jsx(
2309
+ Layouts.Header,
2310
+ {
2311
+ title: formatMessage({
2312
+ id: getTranslation("users.list.header.title"),
2313
+ defaultMessage: "Users"
2314
+ }),
2315
+ subtitle: data?.onlineUsers ? formatMessage(
2316
+ {
2317
+ id: getTranslation("users.list.header.subtitleWithOnline"),
2318
+ defaultMessage: "{total} total · {online} online"
2319
+ },
2320
+ { total: total.toLocaleString(), online: data.onlineUsers }
2321
+ ) : formatMessage(
2322
+ {
2323
+ id: getTranslation("users.list.header.subtitle"),
2324
+ defaultMessage: "{total} total"
2325
+ },
2326
+ { total: total.toLocaleString() }
2327
+ ),
2328
+ primaryAction: rbac.allowedActions.canCreate && /* @__PURE__ */ jsx(
2329
+ Button,
2330
+ {
2331
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
2332
+ onClick: () => setShowCreate(true),
2333
+ "data-testid": "create-user-btn",
2334
+ children: formatMessage({
2335
+ id: getTranslation("users.list.header.create"),
2336
+ defaultMessage: "Create user"
2337
+ })
2338
+ }
2339
+ )
2340
+ }
2341
+ ),
2342
+ /* @__PURE__ */ jsx(
2343
+ Layouts.Action,
2344
+ {
2345
+ startActions: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
2346
+ /* @__PURE__ */ jsx(SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsx(
2347
+ Searchbar,
2348
+ {
2349
+ clearLabel: formatMessage({
2350
+ id: getTranslation("users.list.search.clear"),
2351
+ defaultMessage: "Clear"
2352
+ }),
2353
+ name: "search",
2354
+ placeholder: formatMessage({
2355
+ id: getTranslation("users.list.search.placeholder"),
2356
+ defaultMessage: "Search by email…"
2357
+ }),
2358
+ value: searchInput,
2359
+ onChange: (e) => setSearchInput(e.target.value),
2360
+ onClear: () => {
2361
+ setSearchInput("");
2362
+ setSearch("");
2363
+ setPage(1);
2364
+ },
2365
+ "data-testid": "user-search",
2366
+ children: formatMessage({
2367
+ id: getTranslation("users.list.search.label"),
2368
+ defaultMessage: "Search users"
2369
+ })
2370
+ }
2371
+ ) }),
2372
+ someSelected && rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(
2373
+ Button,
2374
+ {
2375
+ variant: "danger-light",
2376
+ size: "S",
2377
+ onClick: () => setConfirmDeleteMany(true),
2378
+ "data-testid": "delete-selected-btn",
2379
+ children: formatMessage(
2380
+ {
2381
+ id: getTranslation("users.list.actions.deleteSelected"),
2382
+ defaultMessage: "Delete {count} selected"
2383
+ },
2384
+ { count: selected.size }
2385
+ )
2386
+ }
2387
+ ),
2388
+ someSelected && banEnabled && rbac.allowedActions.canUpdate && /* @__PURE__ */ jsx(
2389
+ Button,
2390
+ {
2391
+ variant: "secondary",
2392
+ size: "S",
2393
+ onClick: () => setConfirmBanMany(true),
2394
+ children: formatMessage(
2395
+ {
2396
+ id: getTranslation("users.list.actions.banSelected"),
2397
+ defaultMessage: "Ban {count} selected"
2398
+ },
2399
+ { count: selected.size }
2400
+ )
2401
+ }
2402
+ )
2403
+ ] })
2404
+ }
2405
+ ),
2406
+ /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
2407
+ isError && /* @__PURE__ */ jsx(
2408
+ Alert,
2409
+ {
2410
+ closeLabel: formatMessage({
2411
+ id: getTranslation("users.list.alert.close"),
2412
+ defaultMessage: "Close"
2413
+ }),
2414
+ title: formatMessage({
2415
+ id: getTranslation("users.list.alert.error"),
2416
+ defaultMessage: "Error"
2417
+ }),
2418
+ variant: "danger",
2419
+ children: error?.message
2420
+ }
2421
+ ),
2422
+ isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: formatMessage({
2423
+ id: getTranslation("users.list.loading"),
2424
+ defaultMessage: "Loading users…"
2425
+ }) }) }) : /* @__PURE__ */ jsxs(Table, { colCount, rowCount: users.length + 1, children: [
2426
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
2427
+ canSelect && /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(
2428
+ Checkbox,
2429
+ {
2430
+ checked: allSelected ? true : someSelected ? "indeterminate" : false,
2431
+ onCheckedChange: handleSelectAll,
2432
+ "aria-label": formatMessage({
2433
+ id: getTranslation("users.list.table.selectAll"),
2434
+ defaultMessage: "Select all"
2435
+ })
2436
+ }
2437
+ ) }),
2438
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({
2439
+ id: getTranslation("users.list.table.headers.name"),
2440
+ defaultMessage: "Name"
2441
+ }) }) }),
2442
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({
2443
+ id: getTranslation("users.list.table.headers.email"),
2444
+ defaultMessage: "Email"
2445
+ }) }) }),
2446
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({
2447
+ id: getTranslation("users.list.table.headers.status"),
2448
+ defaultMessage: "Status"
2449
+ }) }) }),
2450
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({
2451
+ id: getTranslation("users.list.table.headers.created"),
2452
+ defaultMessage: "Created"
2453
+ }) }) }),
2454
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(VisuallyHidden, { children: formatMessage({
2455
+ id: getTranslation("users.list.table.headers.actions"),
2456
+ defaultMessage: "Actions"
2457
+ }) }) })
2458
+ ] }) }),
2459
+ /* @__PURE__ */ jsx(Tbody, { children: users.length === 0 ? /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: colCount, children: /* @__PURE__ */ jsx(
2460
+ Flex,
2461
+ {
2462
+ justifyContent: "center",
2463
+ padding: 8,
2464
+ "data-testid": "users-empty",
2465
+ children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: search ? formatMessage(
2466
+ {
2467
+ id: getTranslation("users.list.empty.search"),
2468
+ defaultMessage: 'No users matching "{search}"'
2469
+ },
2470
+ { search }
2471
+ ) : formatMessage({
2472
+ id: getTranslation("users.list.empty.default"),
2473
+ defaultMessage: "No users found"
2474
+ }) })
2475
+ }
2476
+ ) }) }) : users.map((user) => /* @__PURE__ */ jsxs(Tr, { "data-testid": "user-row", children: [
2477
+ canSelect && /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(
2478
+ Checkbox,
2479
+ {
2480
+ checked: selected.has(user.id),
2481
+ onCheckedChange: () => toggleSelect(user.id),
2482
+ "aria-label": formatMessage(
2483
+ {
2484
+ id: getTranslation(
2485
+ "users.list.table.selectRow"
2486
+ ),
2487
+ defaultMessage: "Select {name}"
2488
+ },
2489
+ { name: user.name }
2490
+ )
2491
+ }
2492
+ ) }),
2493
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
2494
+ /* @__PURE__ */ jsx(
2495
+ Avatar,
2496
+ {
2497
+ name: user.name ?? "",
2498
+ src: user.image,
2499
+ size: 28
2500
+ }
2501
+ ),
2502
+ /* @__PURE__ */ jsx(
2503
+ Typography,
2504
+ {
2505
+ fontWeight: "semiBold",
2506
+ textColor: "neutral800",
2507
+ children: user.name
2508
+ }
2509
+ )
2510
+ ] }) }),
2511
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: user.email }) }),
2512
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
2513
+ user.emailVerified ? /* @__PURE__ */ jsx(Badge, { variant: "success", size: "S", children: formatMessage({
2514
+ id: getTranslation(
2515
+ "users.list.status.verified"
2516
+ ),
2517
+ defaultMessage: "Verified"
2518
+ }) }) : /* @__PURE__ */ jsx(Badge, { variant: "neutral", size: "S", children: formatMessage({
2519
+ id: getTranslation(
2520
+ "users.list.status.unverified"
2521
+ ),
2522
+ defaultMessage: "Unverified"
2523
+ }) }),
2524
+ user.banned && /* @__PURE__ */ jsx(Badge, { variant: "danger", size: "S", children: formatMessage({
2525
+ id: getTranslation("users.list.status.banned"),
2526
+ defaultMessage: "Banned"
2527
+ }) })
2528
+ ] }) }),
2529
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: new Date(user.createdAt).toLocaleDateString() }) }),
2530
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
2531
+ rbac.allowedActions.canUpdate && /* @__PURE__ */ jsx(
2532
+ IconButton,
2533
+ {
2534
+ label: formatMessage({
2535
+ id: getTranslation("users.list.table.editUser"),
2536
+ defaultMessage: "Edit user"
2537
+ }),
2538
+ variant: "ghost",
2539
+ onClick: () => setDetailUserId(user.id),
2540
+ "data-testid": "edit-user-btn",
2541
+ children: /* @__PURE__ */ jsx(Pencil, {})
2542
+ }
2543
+ ),
2544
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(
2545
+ IconButton,
2546
+ {
2547
+ label: formatMessage({
2548
+ id: getTranslation(
2549
+ "users.list.table.deleteUser"
2550
+ ),
2551
+ defaultMessage: "Delete user"
2552
+ }),
2553
+ variant: "ghost",
2554
+ onClick: () => setConfirmDelete(user.id),
2555
+ "data-testid": "delete-user-btn",
2556
+ children: /* @__PURE__ */ jsx(Trash, {})
2557
+ }
2558
+ )
2559
+ ] }) })
2560
+ ] }, user.id)) })
2561
+ ] }),
2562
+ pageCount > 1 && /* @__PURE__ */ jsxs(Flex, { justifyContent: "flex-end", gap: 2, children: [
2563
+ /* @__PURE__ */ jsx(
2564
+ Button,
2565
+ {
2566
+ variant: "tertiary",
2567
+ size: "S",
2568
+ disabled: page === 1,
2569
+ onClick: () => setPage((p) => p - 1),
2570
+ children: formatMessage({
2571
+ id: getTranslation("users.list.pagination.previous"),
2572
+ defaultMessage: "Previous"
2573
+ })
2574
+ }
2575
+ ),
2576
+ /* @__PURE__ */ jsx(
2577
+ Button,
2578
+ {
2579
+ variant: "tertiary",
2580
+ size: "S",
2581
+ disabled: page >= pageCount,
2582
+ onClick: () => setPage((p) => p + 1),
2583
+ children: formatMessage({
2584
+ id: getTranslation("users.list.pagination.next"),
2585
+ defaultMessage: "Next"
2586
+ })
2587
+ }
2588
+ )
2589
+ ] })
2590
+ ] }) }),
2591
+ rbac.allowedActions.canCreate && showCreate && /* @__PURE__ */ jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
2592
+ rbac.allowedActions.canUpdate && detailUserId && /* @__PURE__ */ jsx(
2593
+ UserDetailDrawer,
2594
+ {
2595
+ userId: detailUserId,
2596
+ banEnabled,
2597
+ emailVerificationEnabled,
2598
+ twoFactorEnabled,
2599
+ onClose: () => setDetailUserId(null)
2600
+ }
2601
+ ),
2602
+ rbac.allowedActions.canDelete && confirmDelete && /* @__PURE__ */ jsx(
2603
+ ConfirmDialog,
2604
+ {
2605
+ title: formatMessage({
2606
+ id: getTranslation("users.list.confirmDelete.title"),
2607
+ defaultMessage: "Delete user"
2608
+ }),
2609
+ message: formatMessage({
2610
+ id: getTranslation("users.list.confirmDelete.message"),
2611
+ defaultMessage: "Are you sure you want to delete this user? This action cannot be undone."
2612
+ }),
2613
+ confirmLabel: formatMessage({
2614
+ id: getTranslation("users.list.confirmDelete.confirm"),
2615
+ defaultMessage: "Delete"
2616
+ }),
2617
+ loading: deleteMutation.isLoading,
2618
+ onConfirm: () => deleteMutation.mutate(confirmDelete),
2619
+ onCancel: () => setConfirmDelete(null)
2620
+ }
2621
+ ),
2622
+ rbac.allowedActions.canDelete && confirmDeleteMany && /* @__PURE__ */ jsx(
2623
+ ConfirmDialog,
2624
+ {
2625
+ title: formatMessage(
2626
+ {
2627
+ id: getTranslation("users.list.confirmDeleteMany.title"),
2628
+ defaultMessage: "Delete {count, plural, one {# user} other {# users}}"
2629
+ },
2630
+ { count: selected.size }
2631
+ ),
2632
+ message: formatMessage(
2633
+ {
2634
+ id: getTranslation("users.list.confirmDeleteMany.message"),
2635
+ defaultMessage: "Are you sure you want to delete {count, plural, one {# user} other {# users}}? This action cannot be undone."
2636
+ },
2637
+ { count: selected.size }
2638
+ ),
2639
+ confirmLabel: formatMessage({
2640
+ id: getTranslation("users.list.confirmDeleteMany.confirm"),
2641
+ defaultMessage: "Delete all"
2642
+ }),
2643
+ loading: deleteManyMutation.isLoading,
2644
+ onConfirm: () => deleteManyMutation.mutate([...selected]),
2645
+ onCancel: () => setConfirmDeleteMany(false)
2646
+ }
2647
+ ),
2648
+ rbac.allowedActions.canUpdate && confirmBanMany && /* @__PURE__ */ jsx(
2649
+ ConfirmDialog,
2650
+ {
2651
+ title: formatMessage(
2652
+ {
2653
+ id: getTranslation("users.list.confirmBanMany.title"),
2654
+ defaultMessage: "Ban {count, plural, one {# user} other {# users}}"
2655
+ },
2656
+ { count: selected.size }
2657
+ ),
2658
+ message: formatMessage(
2659
+ {
2660
+ id: getTranslation("users.list.confirmBanMany.message"),
2661
+ defaultMessage: "Are you sure you want to ban {count, plural, one {# user} other {# users}}? They will be prevented from signing in."
2662
+ },
2663
+ { count: selected.size }
2664
+ ),
2665
+ confirmLabel: formatMessage({
2666
+ id: getTranslation("users.list.confirmBanMany.confirm"),
2667
+ defaultMessage: "Ban all"
2668
+ }),
2669
+ variant: "danger",
2670
+ loading: banManyMutation.isLoading,
2671
+ onConfirm: () => banManyMutation.mutate([...selected]),
2672
+ onCancel: () => setConfirmBanMany(false)
2673
+ }
2674
+ )
2675
+ ] });
2676
+ }
2677
+ function ProtectedUsersPage() {
2678
+ const { formatMessage } = useIntl();
2679
+ const { data: config, isError, error } = useDashConfig();
2680
+ if (isError) {
2681
+ return /* @__PURE__ */ jsx(Flex, { padding: 6, children: /* @__PURE__ */ jsx(
2682
+ Alert,
2683
+ {
2684
+ closeLabel: formatMessage({
2685
+ id: getTranslation("users.config.close"),
2686
+ defaultMessage: "Close"
2687
+ }),
2688
+ title: formatMessage({
2689
+ id: getTranslation("users.config.errorTitle"),
2690
+ defaultMessage: "Error loading configuration"
2691
+ }),
2692
+ variant: "danger",
2693
+ children: error.message
2694
+ }
2695
+ ) });
2696
+ }
2697
+ if (!config) {
2698
+ return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsx(Loader, { children: formatMessage({
2699
+ id: getTranslation("users.config.loading"),
2700
+ defaultMessage: "Loading user configuration…"
2701
+ }) }) });
2702
+ }
2703
+ return /* @__PURE__ */ jsx(
2704
+ Page.Protect,
2705
+ {
2706
+ permissions: PERMISSIONS.user.filter(
2707
+ (p) => p.action === `plugin::${PLUGIN_ID}.user.read`
2708
+ ),
2709
+ children: /* @__PURE__ */ jsx(UsersPage, { config })
2710
+ }
2711
+ );
2712
+ }
2713
+ export {
2714
+ UsersPage,
2715
+ ProtectedUsersPage as default
2716
+ };