@hed-hog/core 0.0.321 → 0.0.322

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 (64) hide show
  1. package/dist/auth/auth.controller.d.ts +3 -3
  2. package/dist/auth/auth.service.d.ts +8 -8
  3. package/dist/core.module.d.ts.map +1 -1
  4. package/dist/core.module.js +3 -0
  5. package/dist/core.module.js.map +1 -1
  6. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  7. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  8. package/dist/file/file.controller.d.ts +2 -2
  9. package/dist/file/file.service.d.ts +4 -4
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/mail-profile/dto/create.dto.d.ts +23 -0
  15. package/dist/mail-profile/dto/create.dto.d.ts.map +1 -0
  16. package/dist/mail-profile/dto/create.dto.js +139 -0
  17. package/dist/mail-profile/dto/create.dto.js.map +1 -0
  18. package/dist/mail-profile/dto/update.dto.d.ts +6 -0
  19. package/dist/mail-profile/dto/update.dto.d.ts.map +1 -0
  20. package/dist/mail-profile/dto/update.dto.js +9 -0
  21. package/dist/mail-profile/dto/update.dto.js.map +1 -0
  22. package/dist/mail-profile/mail-profile.controller.d.ts +94 -0
  23. package/dist/mail-profile/mail-profile.controller.d.ts.map +1 -0
  24. package/dist/mail-profile/mail-profile.controller.js +95 -0
  25. package/dist/mail-profile/mail-profile.controller.js.map +1 -0
  26. package/dist/mail-profile/mail-profile.module.d.ts +3 -0
  27. package/dist/mail-profile/mail-profile.module.d.ts.map +1 -0
  28. package/dist/mail-profile/mail-profile.module.js +31 -0
  29. package/dist/mail-profile/mail-profile.module.js.map +1 -0
  30. package/dist/mail-profile/mail-profile.service.d.ts +109 -0
  31. package/dist/mail-profile/mail-profile.service.d.ts.map +1 -0
  32. package/dist/mail-profile/mail-profile.service.js +278 -0
  33. package/dist/mail-profile/mail-profile.service.js.map +1 -0
  34. package/dist/session/session.controller.d.ts +1 -1
  35. package/dist/session/session.service.d.ts +3 -3
  36. package/dist/user/user.controller.d.ts +18 -3
  37. package/dist/user/user.controller.d.ts.map +1 -1
  38. package/dist/user/user.controller.js +22 -0
  39. package/dist/user/user.controller.js.map +1 -1
  40. package/dist/user/user.service.d.ts +22 -7
  41. package/dist/user/user.service.d.ts.map +1 -1
  42. package/dist/user/user.service.js +99 -0
  43. package/dist/user/user.service.js.map +1 -1
  44. package/hedhog/data/menu.yaml +35 -3
  45. package/hedhog/data/route.yaml +64 -0
  46. package/hedhog/frontend/app/mail/page.tsx.ejs +5 -0
  47. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +828 -0
  48. package/hedhog/frontend/app/users/page.tsx.ejs +25 -1383
  49. package/hedhog/frontend/app/users/permissions.tsx.ejs +109 -55
  50. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +304 -0
  51. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +1385 -0
  52. package/hedhog/frontend/messages/en.json +91 -0
  53. package/hedhog/frontend/messages/pt.json +91 -0
  54. package/hedhog/table/mail_profile.yaml +75 -0
  55. package/package.json +4 -4
  56. package/src/core.module.ts +3 -0
  57. package/src/index.ts +2 -0
  58. package/src/mail-profile/dto/create.dto.ts +145 -0
  59. package/src/mail-profile/dto/update.dto.ts +4 -0
  60. package/src/mail-profile/mail-profile.controller.ts +56 -0
  61. package/src/mail-profile/mail-profile.module.ts +18 -0
  62. package/src/mail-profile/mail-profile.service.ts +308 -0
  63. package/src/user/user.controller.ts +13 -0
  64. package/src/user/user.service.ts +98 -5
@@ -8,86 +8,22 @@ import {
8
8
  SearchBar,
9
9
  StatsCards,
10
10
  } from '@/components/entity-list';
11
- import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
12
- import {
13
- AlertDialog,
14
- AlertDialogAction,
15
- AlertDialogCancel,
16
- AlertDialogContent,
17
- AlertDialogDescription,
18
- AlertDialogFooter,
19
- AlertDialogHeader,
20
- AlertDialogTitle,
21
- } from '@/components/ui/alert-dialog';
22
11
  import { Button } from '@/components/ui/button';
23
12
  import {
24
13
  Card,
25
- CardContent,
26
14
  CardDescription,
27
15
  CardHeader,
28
16
  CardTitle,
29
17
  } from '@/components/ui/card';
30
- import {
31
- Form,
32
- FormControl,
33
- FormField,
34
- FormItem,
35
- FormLabel,
36
- FormMessage,
37
- } from '@/components/ui/form';
38
- import { FormActions } from '@/components/ui/form-actions';
39
- import { Input } from '@/components/ui/input';
40
- import {
41
- Sheet,
42
- SheetContent,
43
- SheetDescription,
44
- SheetHeader,
45
- SheetTitle,
46
- } from '@/components/ui/sheet';
47
- import { useFormDraft } from '@/hooks/use-form-draft';
48
- import { formatDateTime } from '@/lib/format-date';
49
18
  import { getPhotoUrl } from '@/lib/get-photo-url';
50
19
  import { getUserEmail } from '@/lib/get-user-email';
51
20
  import { PaginatedResult } from '@/types/pagination-result';
52
21
  import { User, UserMfa } from '@hed-hog/api-types';
53
22
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
54
- import { zodResolver } from '@hookform/resolvers/zod';
55
- import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar';
56
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@radix-ui/react-tabs';
57
- import { formatDistanceToNow } from 'date-fns';
58
- import { enUS, ptBR } from 'date-fns/locale';
59
- import {
60
- Activity,
61
- Camera,
62
- Clock,
63
- Eye,
64
- EyeOff,
65
- Fingerprint,
66
- Key,
67
- KeyRound,
68
- LogOut,
69
- LucideIcon,
70
- Mail,
71
- Monitor,
72
- RefreshCcw,
73
- Save,
74
- Shield,
75
- ShieldCheck,
76
- ShieldOff,
77
- Trash2,
78
- UserCircle,
79
- UserIcon,
80
- UserPlus,
81
- Users,
82
- } from 'lucide-react';
23
+ import { Mail, ShieldCheck, UserPlus, Users } from 'lucide-react';
83
24
  import { useTranslations } from 'next-intl';
84
- import { useEffect, useMemo, useState } from 'react';
85
- import { useForm, useWatch } from 'react-hook-form';
86
- import { toast } from 'sonner';
87
- import { z } from 'zod';
88
- import { ActiveSessions } from './active-session';
89
- import { UserIdentifiersSection } from './identifiers';
90
- import { PermissionsSection } from './permissions';
25
+ import { useState } from 'react';
26
+ import { UserSheet } from './user-sheet';
91
27
 
92
28
  type RequestResponse<T> = {
93
29
  paginate: PaginatedResult<T>;
@@ -98,46 +34,13 @@ type RequestResponse<T> = {
98
34
  };
99
35
  };
100
36
 
101
- type UserCreateDraftPayload = {
102
- values: {
103
- name: string;
104
- email: string;
105
- };
106
- };
107
-
108
- type UserEditDraftPayload = {
109
- userId: number | null;
110
- values: {
111
- name: string;
112
- email: string;
113
- };
114
- };
115
-
116
- const USER_CREATE_DRAFT_STORAGE_KEY = 'core-user-create-draft';
117
- const USER_EDIT_DRAFT_STORAGE_KEY = 'core-user-edit-draft';
118
-
119
37
  export default function UserPage() {
120
38
  const t = useTranslations('core.UserPage');
121
- const userActivityT = useTranslations('core.UserActivity');
122
- const { request, currentLocaleCode, getSettingValue } = useApp();
123
- const minPasswordLength = Number(getSettingValue('password-min-length')) || 6;
39
+ const { request, currentLocaleCode } = useApp();
124
40
 
125
41
  const [searchQuery, setSearchQuery] = useState('');
126
- const [isDialogOpen, setIsDialogOpen] = useState(false);
127
- const [showPassword, setShowPassword] = useState(false);
128
- const [editingUser, setEditingUser] = useState<User | null>(null);
129
- const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
130
- const [formError, setFormError] = useState<string | null>(null);
131
- const [editFormError, setEditFormError] = useState<string | null>(null);
132
- const [resetFormError, setResetFormError] = useState<string | null>(null);
133
- const [isResetPasswordDialogOpen, setIsResetPasswordDialogOpen] =
134
- useState(false);
135
- const [isResetResultDialogOpen, setIsResetResultDialogOpen] = useState(false);
136
- const [isResettingPassword, setIsResettingPassword] = useState(false);
137
- const [showResetPassword, setShowResetPassword] = useState(false);
138
- const [showResetPasswordResult, setShowResetPasswordResult] = useState(false);
139
- const [resetPasswordResult, setResetPasswordResult] = useState('');
140
- const [photo, setPhoto] = useState<number | null | undefined>(null);
42
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
43
+ const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
141
44
 
142
45
  const [page, setPage] = useState(1);
143
46
  const [pageSize, setPageSize] = useState(12);
@@ -175,399 +78,21 @@ export default function UserPage() {
175
78
  },
176
79
  });
177
80
 
178
- const addUserSchema = z.object({
179
- name: z.string().min(2, t('errorName')),
180
- email: z.string().email(t('errorEmail')),
181
- password: z
182
- .string()
183
- .min(6, t('errorPassword'))
184
- .optional()
185
- .or(z.literal('')),
186
- });
187
-
188
- const form = useForm<z.infer<typeof addUserSchema>>({
189
- resolver: zodResolver(addUserSchema),
190
- defaultValues: {
191
- name: '',
192
- email: '',
193
- password: '',
194
- },
195
- });
196
-
197
- const editUserSchema = z.object({
198
- name: z.string().min(2, t('errorName')),
199
- email: z.string().email(t('errorEmail')),
200
- });
201
-
202
- const editForm = useForm({
203
- resolver: zodResolver(editUserSchema),
204
- defaultValues: {
205
- name: '',
206
- email: '',
207
- },
208
- });
209
-
210
- const resetPasswordSchema = z.object({
211
- password: z.string().min(minPasswordLength, t('errorPassword')),
212
- });
213
-
214
- const resetPasswordForm = useForm<z.infer<typeof resetPasswordSchema>>({
215
- resolver: zodResolver(resetPasswordSchema),
216
- defaultValues: {
217
- password: '',
218
- },
219
- });
220
-
221
- const watchedCreateValues = useWatch({
222
- control: form.control,
223
- });
224
- const watchedEditValues = useWatch({
225
- control: editForm.control,
226
- });
227
-
228
- const {
229
- clearDraft: clearCreateDraft,
230
- loadDraft: loadCreateDraft,
231
- hasDraft: hasCreateDraft,
232
- savedAt: createDraftSavedAt,
233
- } = useFormDraft<UserCreateDraftPayload>({
234
- storageKey: USER_CREATE_DRAFT_STORAGE_KEY,
235
- value: {
236
- values: {
237
- name: watchedCreateValues.name ?? '',
238
- email: watchedCreateValues.email ?? '',
239
- },
240
- },
241
- hasData: Boolean(
242
- (watchedCreateValues.name ?? '').trim() ||
243
- (watchedCreateValues.email ?? '').trim()
244
- ),
245
- enabled: isDialogOpen,
246
- });
247
-
248
- const {
249
- clearDraft: clearEditDraft,
250
- loadDraft: loadEditDraft,
251
- hasDraft: hasEditDraft,
252
- savedAt: editDraftSavedAt,
253
- } = useFormDraft<UserEditDraftPayload>({
254
- storageKey: USER_EDIT_DRAFT_STORAGE_KEY,
255
- value: {
256
- userId: editingUser?.id ? Number(editingUser.id) : null,
257
- values: {
258
- name: watchedEditValues.name ?? '',
259
- email: watchedEditValues.email ?? '',
260
- },
261
- },
262
- hasData: Boolean(
263
- (watchedEditValues.name ?? '').trim() ||
264
- (watchedEditValues.email ?? '').trim()
265
- ),
266
- enabled: Boolean(editingUser),
267
- });
268
-
269
- const createDraftStatusContent = useMemo(() => {
270
- if (!hasCreateDraft || !createDraftSavedAt) {
271
- return null;
272
- }
273
-
274
- const savedDate = new Date(createDraftSavedAt);
275
-
276
- if (Number.isNaN(savedDate.getTime())) {
277
- return null;
278
- }
279
-
280
- const locale = currentLocaleCode === 'en' ? enUS : ptBR;
281
- const relativeLabel = formatDistanceToNow(savedDate, {
282
- addSuffix: true,
283
- locale,
284
- });
285
- const absoluteLabel = formatDateTime(
286
- savedDate,
287
- getSettingValue,
288
- currentLocaleCode
289
- );
290
-
291
- return currentLocaleCode === 'en'
292
- ? `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`
293
- : `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`;
294
- }, [createDraftSavedAt, currentLocaleCode, getSettingValue, hasCreateDraft]);
295
-
296
- const editDraftStatusContent = useMemo(() => {
297
- if (!hasEditDraft || !editDraftSavedAt) {
298
- return null;
299
- }
300
-
301
- const savedDate = new Date(editDraftSavedAt);
302
-
303
- if (Number.isNaN(savedDate.getTime())) {
304
- return null;
305
- }
306
-
307
- const locale = currentLocaleCode === 'en' ? enUS : ptBR;
308
- const relativeLabel = formatDistanceToNow(savedDate, {
309
- addSuffix: true,
310
- locale,
311
- });
312
- const absoluteLabel = formatDateTime(
313
- savedDate,
314
- getSettingValue,
315
- currentLocaleCode
316
- );
317
-
318
- return currentLocaleCode === 'en'
319
- ? `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`
320
- : `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`;
321
- }, [editDraftSavedAt, currentLocaleCode, getSettingValue, hasEditDraft]);
322
-
323
- const [activeTab, setActiveTab] = useState('overview');
324
- const [openDeleteModal, setOpenDeleteModal] = useState(false);
325
-
326
- const generateRandomPassword = (length = 16) => {
327
- const lowercase = 'abcdefghijkmnopqrstuvwxyz';
328
- const uppercase = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
329
- const numbers = '23456789';
330
- const symbols = '@#$%&*!?-_+';
331
- const allChars = `${lowercase}${uppercase}${numbers}${symbols}`;
332
-
333
- const groups = [lowercase, uppercase, numbers, symbols];
334
- const randomValues = new Uint32Array(length + groups.length);
335
- globalThis.crypto.getRandomValues(randomValues);
336
-
337
- const chars: string[] = [];
338
-
339
- groups.forEach((group, index) => {
340
- const randomValue = randomValues[index] ?? 0;
341
- chars.push(group.charAt(randomValue % group.length));
342
- });
343
-
344
- for (let i = groups.length; i < randomValues.length; i++) {
345
- const randomValue = randomValues[i] ?? 0;
346
- chars.push(allChars.charAt(randomValue % allChars.length));
347
- }
348
-
349
- for (let i = chars.length - 1; i > 0; i--) {
350
- const randomValue = randomValues[i] ?? 0;
351
- const j = randomValue % (i + 1);
352
- const currentChar = chars[i]!;
353
- const swapChar = chars[j]!;
354
- chars[i] = swapChar;
355
- chars[j] = currentChar;
356
- }
357
-
358
- return chars.slice(0, length).join('');
81
+ const handleOpenCreate = () => {
82
+ setSelectedUserId(null);
83
+ setIsSheetOpen(true);
359
84
  };
360
85
 
361
- const openResetPasswordDialog = () => {
362
- const generatedPassword = generateRandomPassword();
363
- resetPasswordForm.reset({ password: generatedPassword });
364
- setResetFormError(null);
365
- setShowResetPassword(false);
366
- setIsResetPasswordDialogOpen(true);
86
+ const handleOpenEdit = (user: User) => {
87
+ setSelectedUserId(Number(user.id));
88
+ setIsSheetOpen(true);
367
89
  };
368
90
 
369
- useEffect(() => {
370
- if (editingUser) {
371
- const storedDraft = loadEditDraft();
372
- const shouldRestoreDraft =
373
- storedDraft?.payload.userId === Number(editingUser.id);
374
-
375
- editForm.reset(
376
- shouldRestoreDraft
377
- ? storedDraft.payload.values
378
- : {
379
- name: editingUser.name || '',
380
- email: getUserEmail(editingUser),
381
- }
382
- );
383
- setActiveTab('overview');
384
- }
385
- }, [editForm, editingUser, loadEditDraft]);
386
-
387
- useEffect(() => {
388
- if (!isDialogOpen) {
389
- return;
390
- }
391
-
392
- const storedDraft = loadCreateDraft();
393
-
394
- form.reset({
395
- name: storedDraft?.payload.values.name ?? '',
396
- email: storedDraft?.payload.values.email ?? '',
397
- password: '',
398
- });
399
- }, [form, isDialogOpen, loadCreateDraft]);
400
-
401
- const onSubmit = async (values: z.infer<typeof addUserSchema>) => {
402
- try {
403
- await request({
404
- url: '/user',
405
- method: 'POST',
406
- data: values,
407
- });
408
- clearCreateDraft();
409
- form.reset();
410
- refetch();
411
- setIsDialogOpen(false);
412
- setFormError(null);
413
- } catch (err) {
414
- const e: any = err;
415
- const msg =
416
- e?.response?.data?.message ||
417
- e?.response?.data?.error ||
418
- e?.message ||
419
- 'Server error';
420
- setFormError(String(msg));
421
- }
422
- };
423
-
424
- const handleEdit = (user: User) => {
425
- setEditFormError(null);
426
- setEditingUser(user);
427
- };
428
-
429
- const onDelete = async () => {
430
- try {
431
- await request({
432
- url: `/user`,
433
- method: 'DELETE',
434
- data: {
435
- ids: [Number(editingUser?.id)],
436
- },
437
- });
438
- refetch();
439
- setOpenDeleteModal(false);
440
- setEditingUser(null);
441
- setEditFormError(null);
442
- toast.success(t('userDeletedSuccess'));
443
- } catch (err) {
444
- const e: any = err;
445
- const msg =
446
- e?.response?.data?.message ||
447
- e?.response?.data?.error ||
448
- e?.message ||
449
- t('serverError');
450
- setEditFormError(String(msg));
451
- }
452
- };
453
-
454
- const handleRefreshEditingUser = async () => {
455
- const updated = await refetch();
456
- const freshUser = updated.data?.paginate?.data.find(
457
- (u: User) => u.id === editingUser?.id
458
- );
459
-
460
- if (freshUser) {
461
- setEditingUser(freshUser);
462
- }
463
- };
464
-
465
- const onEditSubmit = async (values: z.infer<typeof editUserSchema>) => {
466
- try {
467
- await request({
468
- url: `/user/${editingUser?.id}`,
469
- method: 'PATCH',
470
- data: values,
471
- });
472
- clearEditDraft();
473
- refetch();
474
- setEditFormError(null);
475
- toast.success(t('userUpdatedSuccess'));
476
- } catch (err) {
477
- const e: any = err;
478
- const msg =
479
- e?.response?.data?.message ||
480
- e?.response?.data?.error ||
481
- e?.message ||
482
- t('serverError');
483
- setEditFormError(String(msg));
484
- }
485
- };
486
-
487
- const onResetPasswordSubmit = async () => {
488
- if (!editingUser?.id) {
489
- return;
490
- }
491
-
492
- setIsResettingPassword(true);
493
- setResetFormError(null);
494
- try {
495
- const { password } = resetPasswordForm.getValues();
496
- const response = await request<{ password: string }>({
497
- url: `/user/${editingUser.id}/reset-password`,
498
- method: 'PATCH',
499
- data: {
500
- password,
501
- },
502
- });
503
-
504
- setResetPasswordResult(response.data.password);
505
- setIsResetPasswordDialogOpen(false);
506
- setIsResetResultDialogOpen(true);
507
- refetch();
508
- toast.success(t('passwordResetSuccess'));
509
- } catch (err) {
510
- const e: any = err;
511
- const msg =
512
- e?.response?.data?.message ||
513
- e?.response?.data?.error ||
514
- e?.message ||
515
- t('serverError');
516
- setResetFormError(String(msg));
517
- } finally {
518
- setIsResettingPassword(false);
519
- }
520
- };
521
-
522
- const copyResetPassword = async () => {
523
- try {
524
- await navigator.clipboard.writeText(resetPasswordResult);
525
- toast.success(t('passwordCopied'));
526
- } catch {
527
- toast.error(t('passwordCopyError'));
528
- }
529
- };
530
-
531
- const handleAvatarClick = () => {
532
- const input = document.createElement('input');
533
- input.type = 'file';
534
- input.accept = 'image/*';
535
- input.onchange = async (e: any) => {
536
- const file = e.target?.files?.[0];
537
- if (file && editingUser) {
538
- setIsUploadingAvatar(true);
539
- try {
540
- const formData = new FormData();
541
- formData.append('avatar', file);
542
-
543
- const response: any = await request({
544
- url: `/user/${editingUser.id}/avatar`,
545
- method: 'POST',
546
- data: formData,
547
- headers: {
548
- 'Content-Type': 'multipart/form-data',
549
- },
550
- });
551
-
552
- setPhoto(response.data.id);
553
- toast.success(t('pictureUpdatedSuccess'));
554
- refetch();
555
- } catch (err) {
556
- console.error(err);
557
- } finally {
558
- setIsUploadingAvatar(false);
559
- }
560
- }
561
- };
562
- input.click();
91
+ const handleSheetClose = (open: boolean) => {
92
+ setIsSheetOpen(open);
93
+ if (!open) setSelectedUserId(null);
563
94
  };
564
95
 
565
- const accountProviders = [
566
- { name: 'Google', icon: '/google.svg' },
567
- { name: 'GitHub', icon: '/github.svg' },
568
- { name: 'Facebook', icon: '/facebook.svg' },
569
- ];
570
-
571
96
  return (
572
97
  <Page>
573
98
  <PageHeader
@@ -575,7 +100,7 @@ export default function UserPage() {
575
100
  actions={[
576
101
  {
577
102
  label: t('buttonAddUser'),
578
- onClick: () => setIsDialogOpen(true),
103
+ onClick: handleOpenCreate,
579
104
  variant: 'default',
580
105
  },
581
106
  ]}
@@ -673,14 +198,14 @@ export default function UserPage() {
673
198
  title={t('noUsersFound')}
674
199
  description={t('description')}
675
200
  actionLabel={t('buttonAddUser')}
676
- onAction={() => setIsDialogOpen(true)}
201
+ onAction={handleOpenCreate}
677
202
  />
678
203
  ) : (
679
204
  <div className="grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
680
205
  {paginate?.data?.map((user: User) => (
681
206
  <Card
682
207
  key={user.id}
683
- onDoubleClick={() => handleEdit(user)}
208
+ onDoubleClick={() => handleOpenEdit(user)}
684
209
  className="cursor-pointer rounded-md flex flex-col justify-between gap-2 border border-border/60 bg-card p-4 shadow-sm transition hover:border-primary"
685
210
  >
686
211
  <CardHeader className="flex items-start justify-between gap-4 p-0">
@@ -705,7 +230,7 @@ export default function UserPage() {
705
230
  <Button
706
231
  variant="outline"
707
232
  size="sm"
708
- onClick={() => handleEdit(user)}
233
+ onClick={() => handleOpenEdit(user)}
709
234
  >
710
235
  {t('buttonEditUser')}
711
236
  </Button>
@@ -831,895 +356,12 @@ export default function UserPage() {
831
356
  />
832
357
  </div>
833
358
 
834
- <Sheet open={isDialogOpen} onOpenChange={setIsDialogOpen}>
835
- <SheetContent className="w-full sm:max-w-lg overflow-y-auto gap-0">
836
- <SheetHeader>
837
- <SheetTitle>{t('dialogAddUserTitle')}</SheetTitle>
838
- <SheetDescription>{t('description')}</SheetDescription>
839
- </SheetHeader>
840
- <div className="w-full border-t pt-1 mt-1" />
841
- <Form {...form}>
842
- <form
843
- onSubmit={form.handleSubmit(onSubmit)}
844
- className="px-4 gap-4 w-full flex flex-col pt-2"
845
- >
846
- <FormField
847
- control={form.control}
848
- name="name"
849
- render={({ field }) => (
850
- <FormItem>
851
- <FormLabel>{t('formNameLabel')}</FormLabel>
852
- <FormControl>
853
- <Input {...field} />
854
- </FormControl>
855
- <FormMessage />
856
- </FormItem>
857
- )}
858
- />
859
- <FormField
860
- control={form.control}
861
- name="email"
862
- render={({ field }) => (
863
- <FormItem>
864
- <FormLabel>{t('formEmailLabel')}</FormLabel>
865
- <FormControl>
866
- <Input type="email" {...field} />
867
- </FormControl>
868
- <FormMessage />
869
- </FormItem>
870
- )}
871
- />
872
- <FormField
873
- control={form.control}
874
- name="password"
875
- render={({ field }) => (
876
- <FormItem>
877
- <FormLabel>{t('formPasswordLabel')}</FormLabel>
878
- <FormControl>
879
- <div className="relative">
880
- <Input
881
- type={showPassword ? 'text' : 'password'}
882
- {...field}
883
- className="pr-10"
884
- />
885
- <button
886
- type="button"
887
- onClick={() => setShowPassword((s) => !s)}
888
- className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded p-1 text-sm text-muted-foreground hover:bg-muted/50"
889
- aria-label={
890
- showPassword
891
- ? t('hidePassword')
892
- : t('showPassword')
893
- }
894
- >
895
- {showPassword ? (
896
- <EyeOff className="h-4 w-4" />
897
- ) : (
898
- <Eye className="h-4 w-4" />
899
- )}
900
- </button>
901
- </div>
902
- </FormControl>
903
- <FormMessage />
904
- </FormItem>
905
- )}
906
- />
907
-
908
- {formError && (
909
- <Alert
910
- variant="destructive"
911
- className="border-red-300 bg-red-50 rounded-md p-4"
912
- >
913
- <AlertTitle className="text-sm">
914
- {t('verifyYourInput')}
915
- </AlertTitle>
916
- <AlertDescription className="text-sm">
917
- {formError}
918
- </AlertDescription>
919
- </Alert>
920
- )}
921
-
922
- <FormActions
923
- sheet
924
- statusContent={createDraftStatusContent}
925
- submitLabel={t('buttonAddUser')}
926
- submitType="submit"
927
- sheetClassName="px-0 pb-0"
928
- />
929
- </form>
930
- </Form>
931
- </SheetContent>
932
- </Sheet>
933
-
934
- {editingUser && (
935
- <Sheet
936
- open={!!editingUser}
937
- onOpenChange={(open) => {
938
- setEditingUser(null);
939
- setPhoto(null);
940
- setIsResetPasswordDialogOpen(false);
941
- setIsResetResultDialogOpen(false);
942
- setResetPasswordResult('');
943
- }}
944
- >
945
- <SheetContent className="w-full sm:max-w-4xl overflow-y-auto gap-0">
946
- <SheetHeader>
947
- <SheetTitle>{t('titleEditUser')}</SheetTitle>
948
- <SheetDescription>{t('description')}</SheetDescription>
949
- </SheetHeader>
950
-
951
- <Tabs value={activeTab} onValueChange={setActiveTab}>
952
- <TabsList className="grid w-full grid-cols-7 bg-muted rounded-md text-muted-foreground text-sm">
953
- <TabsTrigger
954
- value="overview"
955
- className="flex items-center justify-center gap-2 px-3 py-4 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
956
- >
957
- <UserCircle className="h-4 w-4 min-h-4 min-w-4" />
958
- <span className="hidden md:inline">{t('tabOverview')}</span>
959
- </TabsTrigger>
960
- <TabsTrigger
961
- value="edit"
962
- className="flex items-center justify-center gap-2 px-3 py-4 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
963
- >
964
- <Save className="h-4 w-4 min-h-4 min-w-4" />
965
- <span className="hidden md:inline">{t('tabEdit')}</span>
966
- </TabsTrigger>
967
- <TabsTrigger
968
- value="credentials"
969
- className="flex items-center justify-center gap-2 px-3 py-4 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
970
- >
971
- <Key className="h-4 w-4 min-h-4 min-w-4" />
972
- <span className="hidden md:inline">
973
- {t('tabCredentials')}
974
- </span>
975
- </TabsTrigger>
976
- <TabsTrigger
977
- value="identifiers"
978
- className="flex items-center justify-center gap-2 px-3 py-4 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
979
- >
980
- <Fingerprint className="h-4 w-4 min-h-4 min-w-4" />
981
- <span className="hidden md:inline">
982
- {t('tabIdentifiers')}
983
- </span>
984
- </TabsTrigger>
985
- <TabsTrigger
986
- value="permissions"
987
- className="flex items-center justify-center gap-2 px-3 py-4 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
988
- >
989
- <ShieldCheck className="h-4 w-4 min-h-4 min-w-4" />
990
- <span className="hidden md:inline">
991
- {t('tabPermissions')}
992
- </span>
993
- </TabsTrigger>
994
- <TabsTrigger
995
- value="mfa"
996
- className="flex items-center justify-center gap-2 px-3 py-4 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
997
- >
998
- <Shield className="h-4 w-4 min-h-4 min-w-4" />
999
- <span className="hidden md:inline">{t('tabMfa')}</span>
1000
- </TabsTrigger>
1001
- <TabsTrigger
1002
- value="sessions"
1003
- className="flex items-center justify-center gap-2 px-3 py-4 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
1004
- >
1005
- <Monitor className="h-4 w-4 min-h-4 min-w-4" />
1006
- <span className="hidden md:inline">{t('tabSessions')}</span>
1007
- </TabsTrigger>
1008
- </TabsList>
1009
-
1010
- <TabsContent
1011
- value="overview"
1012
- className="space-y-4 mt-4 p-4 pt-0"
1013
- >
1014
- <div className="flex flex-col items-center gap-3 py-4">
1015
- <div className="relative group">
1016
- <Avatar className="h-24 w-24 shadow-md">
1017
- <AvatarImage
1018
- src={getPhotoUrl(photo || editingUser.photo_id)}
1019
- alt={editingUser.name}
1020
- className="object-cover h-24 w-24 rounded-full"
1021
- />
1022
- <AvatarFallback className="text-2xl font-semibold bg-linear-to-br from-purple-500 to-pink-500 text-white">
1023
- {editingUser.name?.charAt(0)?.toUpperCase() || 'U'}
1024
- </AvatarFallback>
1025
- </Avatar>
1026
- <button
1027
- onClick={handleAvatarClick}
1028
- disabled={isUploadingAvatar}
1029
- className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer disabled:cursor-not-allowed"
1030
- >
1031
- {isUploadingAvatar ? (
1032
- <div className="h-5 w-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
1033
- ) : (
1034
- <Camera className="h-6 w-6 text-white" />
1035
- )}
1036
- </button>
1037
- <div className="absolute -bottom-1 -right-1">
1038
- {editingUser.suspended_until ? (
1039
- <div className="rounded-full bg-rose-500 p-1.5 shadow-md">
1040
- <ShieldCheck className="h-3.5 w-3.5 text-white" />
1041
- </div>
1042
- ) : (
1043
- <div className="rounded-full bg-green-500 p-1.5 shadow-md">
1044
- <Users className="h-3.5 w-3.5 text-white" />
1045
- </div>
1046
- )}
1047
- </div>
1048
- </div>
1049
- <div className="text-center space-y-0.5">
1050
- <h3 className="text-xl font-bold bg-linear-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
1051
- {editingUser.name}
1052
- </h3>
1053
- <p className="text-sm text-muted-foreground flex items-center gap-2 justify-center">
1054
- <Mail className="h-4 w-4" />
1055
- {getUserEmail(editingUser)}
1056
- </p>
1057
- </div>
1058
- </div>
1059
-
1060
- <div className="grid grid-cols-2 gap-2">
1061
- <Card
1062
- className={`border-l-4 ${editingUser.suspended_until ? 'border-l-red-500' : 'border-l-green-500'} py-2`}
1063
- >
1064
- <CardHeader className="p-2">
1065
- <div className="flex items-center justify-between">
1066
- <div>
1067
- <CardDescription className="text-xs">
1068
- {t('status')}
1069
- </CardDescription>
1070
- <CardTitle className="text-sm">
1071
- {editingUser.suspended_until ? (
1072
- <span className="text-rose-600">
1073
- {t('blocked')}
1074
- </span>
1075
- ) : (
1076
- <span className="text-green-600">
1077
- {t('active')}
1078
- </span>
1079
- )}
1080
- </CardTitle>
1081
- </div>
1082
- <div
1083
- className={`rounded-full p-1 ${editingUser.suspended_until ? 'bg-rose-100' : 'bg-green-100'}`}
1084
- >
1085
- <Activity
1086
- className={`h-6 w-6 ${editingUser.suspended_until ? 'text-rose-600' : 'text-green-600'}`}
1087
- />
1088
- </div>
1089
- </div>
1090
- </CardHeader>
1091
- </Card>
1092
-
1093
- <Card className="border-l-4 border-l-primary-500 py-2">
1094
- <CardHeader className="p-2">
1095
- <div className="flex items-center justify-between">
1096
- <div>
1097
- <CardDescription className="text-xs">
1098
- {t('cardLastLoginDescription')}
1099
- </CardDescription>
1100
- {Boolean(editingUser.user_session?.length) ? (
1101
- <CardTitle className="text-sm truncate">
1102
- {editingUser.user_session?.[0]?.created_at &&
1103
- formatDistanceToNow(
1104
- new Date(
1105
- editingUser.user_session?.[0]?.created_at
1106
- ),
1107
- {
1108
- addSuffix: true,
1109
- locale:
1110
- currentLocaleCode === 'en'
1111
- ? enUS
1112
- : ptBR,
1113
- }
1114
- )}
1115
- <small className="text-xs ml-2 text-muted-foreground">
1116
- {formatDateTime(
1117
- String(
1118
- editingUser.user_session?.[0]?.created_at
1119
- ),
1120
- getSettingValue,
1121
- currentLocaleCode
1122
- )}
1123
- </small>
1124
- </CardTitle>
1125
- ) : (
1126
- <CardTitle>{t('cardLastLoginTitle')}</CardTitle>
1127
- )}
1128
- </div>
1129
- <div className="rounded-full bg-primary-100 p-1">
1130
- <Clock className="h-6 w-6 text-primary-600" />
1131
- </div>
1132
- </div>
1133
- </CardHeader>
1134
- </Card>
1135
- </div>
1136
-
1137
- <div className="space-y-3">
1138
- <div className="flex items-center justify-between">
1139
- <h4 className="text-sm font-semibold flex items-center gap-2">
1140
- <Shield className="h-4 w-4" />
1141
- {t('securityTitle')}
1142
- </h4>
1143
- </div>
1144
-
1145
- <div className="grid grid-cols-3 gap-2">
1146
- <Card className="border-l-4 border-l-amber-500 py-2">
1147
- <CardHeader className="p-2">
1148
- <div className="flex flex-col items-center text-center">
1149
- <div className="rounded-full bg-amber-100 p-1 mb-1">
1150
- <Shield className="h-4 w-4 text-amber-600" />
1151
- </div>
1152
- <CardDescription className="text-xs">
1153
- {t('securityDescription')}
1154
- </CardDescription>
1155
- <CardTitle className="text-sm font-bold">
1156
- {editingUser.user_mfa &&
1157
- editingUser.user_mfa.length > 0 ? (
1158
- <span className="text-amber-600">
1159
- {t('securityLevelHight')}
1160
- </span>
1161
- ) : (
1162
- <span className="text-orange-600">
1163
- {t('securityLevelMedium')}
1164
- </span>
1165
- )}
1166
- </CardTitle>
1167
- </div>
1168
- </CardHeader>
1169
- </Card>
1170
-
1171
- <Card className="border-l-4 border-l-purple-500 py-2">
1172
- <CardHeader className="p-2">
1173
- <div className="flex flex-col items-center text-center">
1174
- <div className="rounded-full bg-purple-100 p-1 mb-1">
1175
- <Shield className="h-4 w-4 text-purple-600" />
1176
- </div>
1177
- <CardDescription className="text-xs">
1178
- {t('cardMfaMethods')}
1179
- </CardDescription>
1180
- <CardTitle className="text-sm font-bold text-purple-600">
1181
- {editingUser.user_mfa?.length || 0}
1182
- </CardTitle>
1183
- </div>
1184
- </CardHeader>
1185
- </Card>
1186
-
1187
- <Card className="border-l-4 border-l-blue-500 py-2">
1188
- <CardHeader className="p-2">
1189
- <div className="flex flex-col items-center text-center">
1190
- <div className="rounded-full bg-blue-100 p-1 mb-1">
1191
- <Fingerprint className="h-4 w-4 text-blue-600" />
1192
- </div>
1193
- <CardDescription className="text-xs">
1194
- {t('cardUserId')}
1195
- </CardDescription>
1196
- <CardTitle className="text-xs font-bold text-blue-600">
1197
- #{editingUser.id}
1198
- </CardTitle>
1199
- </div>
1200
- </CardHeader>
1201
- </Card>
1202
- </div>
1203
- </div>
1204
-
1205
- {editingUser.suspended_until && (
1206
- <Card className="border-rose-200 bg-rose-50/50">
1207
- <CardHeader className="p-2">
1208
- <div className="flex items-start gap-2">
1209
- <div className="rounded-full bg-rose-100 p-1">
1210
- <ShieldCheck className="h-3 w-3 text-rose-600" />
1211
- </div>
1212
- <div className="flex-1 space-y-2">
1213
- <div>
1214
- <p className="text-sm font-semibold text-rose-900">
1215
- {t('accountSuspended')}
1216
- </p>
1217
- <p className="text-xs text-rose-700 flex items-center gap-1 mt-1">
1218
- <Clock className="h-3 w-3" />
1219
- {t('until')}: {editingUser.suspended_until}
1220
- </p>
1221
- </div>
1222
- {editingUser.suspended_reason && (
1223
- <div className="rounded-md bg-rose-100 p-2">
1224
- <p className="text-xs font-medium text-rose-900">
1225
- {t('reason')}
1226
- </p>
1227
- <p className="text-xs text-rose-700">
1228
- {editingUser.suspended_reason}
1229
- </p>
1230
- </div>
1231
- )}
1232
- </div>
1233
- </div>
1234
- </CardHeader>
1235
- </Card>
1236
- )}
1237
-
1238
- <div className="space-y-3">
1239
- <div className="flex items-center justify-between">
1240
- <h4 className="text-sm font-semibold flex items-center gap-2">
1241
- <Shield className="h-4 w-4" />
1242
- {t('recentActivityTitle')}
1243
- </h4>
1244
- </div>
1245
-
1246
- <Card className="rounded-md p-0">
1247
- <CardContent className="p-4 gap-2 flex flex-col">
1248
- {editingUser.user_activity?.length ? (
1249
- <div className="space-y-3">
1250
- {editingUser.user_activity
1251
- ?.slice(0, 5)
1252
- .map((activity, index) => {
1253
- const actionIcons: Record<string, LucideIcon> =
1254
- {
1255
- login: UserIcon,
1256
- forgotPassword: KeyRound,
1257
- logout: LogOut,
1258
- resetPassword: RefreshCcw,
1259
- revokeAllSessions: ShieldOff,
1260
- revokeSession: Shield,
1261
- };
1262
-
1263
- const Icon =
1264
- actionIcons[activity.action] || UserIcon;
1265
-
1266
- return (
1267
- <div
1268
- key={index}
1269
- className="flex items-start gap-3 border-b pb-3 last:border-none"
1270
- >
1271
- <div className="rounded-md bg-blue-50 p-2">
1272
- <Icon className="w-4 h-4 text-gray-600" />
1273
- </div>
1274
- <div className="flex-1">
1275
- <p className="text-sm font-medium">
1276
- {userActivityT(activity.action)}
1277
- </p>
1278
- <p className="text-[11px] text-gray-500 mt-1">
1279
- {activity.created_at &&
1280
- formatDistanceToNow(
1281
- new Date(activity.created_at),
1282
- {
1283
- addSuffix: true,
1284
- locale:
1285
- currentLocaleCode === 'en'
1286
- ? enUS
1287
- : ptBR,
1288
- }
1289
- )}
1290
- </p>
1291
- </div>
1292
- </div>
1293
- );
1294
- })}
1295
- </div>
1296
- ) : (
1297
- <span className="text-sm text-muted-foreground">
1298
- {t('noRecentActivity')}
1299
- </span>
1300
- )}
1301
- </CardContent>
1302
- </Card>
1303
- </div>
1304
-
1305
- <div className="flex justify-end">
1306
- <Button
1307
- className="cursor-pointer"
1308
- variant="destructive"
1309
- onClick={() => setOpenDeleteModal(true)}
1310
- >
1311
- <Trash2 className="w-4 h-4" />
1312
- <span>{t('buttonDeleteUser')}</span>
1313
- </Button>
1314
- </div>
1315
-
1316
- <AlertDialog
1317
- open={openDeleteModal}
1318
- onOpenChange={setOpenDeleteModal}
1319
- >
1320
- <AlertDialogContent>
1321
- <AlertDialogHeader>
1322
- <AlertDialogTitle>
1323
- {t('dialogDeleteUserTitle')}
1324
- </AlertDialogTitle>
1325
- <AlertDialogDescription>
1326
- {t('dialogDeleteUserDescription')}
1327
- </AlertDialogDescription>
1328
- </AlertDialogHeader>
1329
- <AlertDialogFooter>
1330
- <AlertDialogCancel>
1331
- {t('deleteUserCancel')}
1332
- </AlertDialogCancel>
1333
- <AlertDialogAction onClick={onDelete}>
1334
- {t('deleteUserConfirm')}
1335
- </AlertDialogAction>
1336
- </AlertDialogFooter>
1337
- </AlertDialogContent>
1338
- </AlertDialog>
1339
- </TabsContent>
1340
-
1341
- <TabsContent value="edit" className="space-y-4 mt-4 p-4 pt-0">
1342
- <Form {...editForm}>
1343
- <form
1344
- onSubmit={editForm.handleSubmit(onEditSubmit)}
1345
- className="space-y-4"
1346
- >
1347
- <FormField
1348
- control={editForm.control}
1349
- name="name"
1350
- render={({ field }) => (
1351
- <FormItem>
1352
- <FormLabel>{t('editNameLabel')}</FormLabel>
1353
- <FormControl>
1354
- <Input
1355
- placeholder={t('editNamePlaceholder')}
1356
- {...field}
1357
- />
1358
- </FormControl>
1359
- <FormMessage />
1360
- </FormItem>
1361
- )}
1362
- />
1363
- <FormActions
1364
- statusContent={editDraftStatusContent}
1365
- cancelLabel={t('cancel')}
1366
- onCancel={() => setEditingUser(null)}
1367
- submitLabel={t('saveChanges')}
1368
- submitType="submit"
1369
- className="pt-2"
1370
- />
1371
- </form>
1372
- </Form>
1373
- </TabsContent>
1374
-
1375
- <TabsContent
1376
- value="credentials"
1377
- className="space-y-4 mt-4 p-4 pt-0"
1378
- >
1379
- <div className="space-y-3 rounded-lg border p-4">
1380
- <div>
1381
- <h4 className="text-sm font-semibold">
1382
- {t('passwordResetTitle')}
1383
- </h4>
1384
- <p className="text-xs text-muted-foreground mt-1">
1385
- {t('passwordResetDescription')}
1386
- </p>
1387
- </div>
1388
- <p className="text-xs text-muted-foreground">
1389
- {t('passwordResetNotice')}
1390
- </p>
1391
- <div className="flex flex-col gap-2">
1392
- <Button
1393
- type="button"
1394
- onClick={openResetPasswordDialog}
1395
- className="w-full sm:w-fit"
1396
- >
1397
- <RefreshCcw className="h-4 w-4 mr-2" />
1398
- {t('buttonResetPassword')}
1399
- </Button>
1400
- </div>
1401
- </div>
1402
-
1403
- <div className="space-y-3">
1404
- <h4 className="text-sm font-medium">
1405
- {t('connectedAccountsTitle')}
1406
- </h4>
1407
- <div className="space-y-2">
1408
- {accountProviders.map((provider: any) => {
1409
- const isConnected = editingUser.user_account?.includes(
1410
- provider.name.toLowerCase()
1411
- );
1412
-
1413
- return (
1414
- <div
1415
- key={provider.name}
1416
- className={`flex items-center justify-between rounded-lg border p-3 ${
1417
- isConnected ? '' : 'border-dashed'
1418
- }`}
1419
- >
1420
- <div className="flex items-center gap-3">
1421
- <div className="rounded-md bg-slate-50 p-2">
1422
- <img
1423
- src={provider.icon}
1424
- alt={`${provider.name} icon`}
1425
- className={`h-5 w-5 ${
1426
- isConnected ? 'opacity-100' : 'opacity-50'
1427
- }`}
1428
- />
1429
- </div>
1430
- <div>
1431
- <p
1432
- className={`text-sm font-medium ${
1433
- isConnected ? '' : 'text-muted-foreground'
1434
- }`}
1435
- >
1436
- {provider.name}
1437
- </p>
1438
- <p className="text-xs text-muted-foreground">
1439
- {isConnected
1440
- ? t('connected')
1441
- : t('notConnected')}
1442
- </p>
1443
- </div>
1444
- </div>
1445
- </div>
1446
- );
1447
- })}
1448
- </div>
1449
- </div>
1450
-
1451
- <AlertDialog
1452
- open={isResetPasswordDialogOpen}
1453
- onOpenChange={(open) => {
1454
- setIsResetPasswordDialogOpen(open);
1455
- if (!open) {
1456
- setResetFormError(null);
1457
- }
1458
- }}
1459
- >
1460
- <AlertDialogContent>
1461
- <AlertDialogHeader>
1462
- <AlertDialogTitle>
1463
- {t('passwordResetDialogTitle')}
1464
- </AlertDialogTitle>
1465
- <AlertDialogDescription>
1466
- {t('passwordResetDialogDescription')}
1467
- </AlertDialogDescription>
1468
- </AlertDialogHeader>
1469
-
1470
- <Form {...resetPasswordForm}>
1471
- <form className="space-y-4">
1472
- <FormField
1473
- control={resetPasswordForm.control}
1474
- name="password"
1475
- render={({ field }) => (
1476
- <FormItem>
1477
- <FormLabel>
1478
- {t('passwordResetFieldLabel')}
1479
- </FormLabel>
1480
- <FormControl>
1481
- <div className="space-y-2">
1482
- <div className="relative">
1483
- <Input
1484
- type={
1485
- showResetPassword
1486
- ? 'text'
1487
- : 'password'
1488
- }
1489
- {...field}
1490
- className="pr-10"
1491
- />
1492
- <button
1493
- type="button"
1494
- onClick={() =>
1495
- setShowResetPassword(
1496
- (state) => !state
1497
- )
1498
- }
1499
- className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded p-1 text-sm text-muted-foreground hover:bg-muted/50"
1500
- aria-label={
1501
- showResetPassword
1502
- ? t('hidePassword')
1503
- : t('showPassword')
1504
- }
1505
- >
1506
- {showResetPassword ? (
1507
- <EyeOff className="h-4 w-4" />
1508
- ) : (
1509
- <Eye className="h-4 w-4" />
1510
- )}
1511
- </button>
1512
- </div>
1513
-
1514
- <Button
1515
- type="button"
1516
- variant="outline"
1517
- onClick={() => {
1518
- resetPasswordForm.setValue(
1519
- 'password',
1520
- generateRandomPassword(),
1521
- {
1522
- shouldValidate: true,
1523
- shouldDirty: true,
1524
- }
1525
- );
1526
- }}
1527
- >
1528
- <RefreshCcw className="h-4 w-4 mr-2" />
1529
- {t('buttonRegeneratePassword')}
1530
- </Button>
1531
- </div>
1532
- </FormControl>
1533
- <FormMessage />
1534
- </FormItem>
1535
- )}
1536
- />
1537
-
1538
- {resetFormError && (
1539
- <Alert variant="destructive">
1540
- <AlertTitle>{t('verifyYourInput')}</AlertTitle>
1541
- <AlertDescription>
1542
- {resetFormError}
1543
- </AlertDescription>
1544
- </Alert>
1545
- )}
1546
- </form>
1547
- </Form>
1548
-
1549
- <AlertDialogFooter>
1550
- <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
1551
- <AlertDialogAction
1552
- disabled={isResettingPassword}
1553
- onClick={async (event) => {
1554
- event.preventDefault();
1555
- const valid = await resetPasswordForm.trigger();
1556
- if (!valid) {
1557
- return;
1558
- }
1559
-
1560
- await onResetPasswordSubmit();
1561
- }}
1562
- >
1563
- {isResettingPassword
1564
- ? t('passwordResetSubmitting')
1565
- : t('passwordResetConfirm')}
1566
- </AlertDialogAction>
1567
- </AlertDialogFooter>
1568
- </AlertDialogContent>
1569
- </AlertDialog>
1570
-
1571
- <AlertDialog
1572
- open={isResetResultDialogOpen}
1573
- onOpenChange={(open) => {
1574
- setIsResetResultDialogOpen(open);
1575
- if (!open) {
1576
- setResetPasswordResult('');
1577
- setShowResetPasswordResult(false);
1578
- }
1579
- }}
1580
- >
1581
- <AlertDialogContent>
1582
- <AlertDialogHeader>
1583
- <AlertDialogTitle>
1584
- {t('passwordResultTitle')}
1585
- </AlertDialogTitle>
1586
- <AlertDialogDescription>
1587
- {t('passwordResultDescription')}
1588
- </AlertDialogDescription>
1589
- </AlertDialogHeader>
1590
-
1591
- <div className="space-y-3">
1592
- <div className="relative">
1593
- <Input
1594
- readOnly
1595
- value={resetPasswordResult}
1596
- type={showResetPasswordResult ? 'text' : 'password'}
1597
- className="pr-10"
1598
- />
1599
- <button
1600
- type="button"
1601
- onClick={() =>
1602
- setShowResetPasswordResult((state) => !state)
1603
- }
1604
- className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded p-1 text-sm text-muted-foreground hover:bg-muted/50"
1605
- aria-label={
1606
- showResetPasswordResult
1607
- ? t('hidePassword')
1608
- : t('showPassword')
1609
- }
1610
- >
1611
- {showResetPasswordResult ? (
1612
- <EyeOff className="h-4 w-4" />
1613
- ) : (
1614
- <Eye className="h-4 w-4" />
1615
- )}
1616
- </button>
1617
- </div>
1618
-
1619
- <div className="flex justify-end gap-2">
1620
- <Button
1621
- type="button"
1622
- variant="outline"
1623
- onClick={copyResetPassword}
1624
- >
1625
- {t('buttonCopyPassword')}
1626
- </Button>
1627
- <Button
1628
- type="button"
1629
- onClick={() => setIsResetResultDialogOpen(false)}
1630
- >
1631
- {t('close')}
1632
- </Button>
1633
- </div>
1634
- </div>
1635
- </AlertDialogContent>
1636
- </AlertDialog>
1637
- </TabsContent>
1638
-
1639
- <TabsContent
1640
- value="identifiers"
1641
- className="space-y-4 mt-4 p-4 pt-0"
1642
- >
1643
- <UserIdentifiersSection editingUser={editingUser} />
1644
- </TabsContent>
1645
-
1646
- <TabsContent
1647
- value="permissions"
1648
- className="space-y-4 mt-4 p-4 pt-0"
1649
- >
1650
- <div className="space-y-3">
1651
- <div className="flex items-center justify-between">
1652
- <div>
1653
- <h4 className="text-sm font-semibold flex items-center gap-2">
1654
- <ShieldCheck className="h-4 w-4" />
1655
- {t('permissionsTitle')}
1656
- </h4>
1657
- <p className="text-xs text-muted-foreground mt-1">
1658
- {t('permissionsDescription')}
1659
- </p>
1660
- </div>
1661
- </div>
1662
-
1663
- <PermissionsSection
1664
- userId={editingUser.id!}
1665
- onRoleChange={handleRefreshEditingUser}
1666
- />
1667
- </div>
1668
- </TabsContent>
1669
-
1670
- <TabsContent value="mfa" className="space-y-4 mt-4 p-4 pt-0">
1671
- <div className="space-y-3">
1672
- <h4 className="text-sm font-medium">{t('mfaTitle')}</h4>
1673
- {editingUser.user_mfa && editingUser.user_mfa.length > 0 ? (
1674
- <div className="space-y-2">
1675
- {editingUser.user_mfa.map((mfa: UserMfa) => (
1676
- <div
1677
- key={String(mfa.id)}
1678
- className="flex items-center justify-between rounded-lg border p-3"
1679
- >
1680
- <div className="flex items-center gap-3">
1681
- <div className="rounded-md bg-amber-50 p-2">
1682
- <Shield className="h-5 w-5 text-amber-600" />
1683
- </div>
1684
- <div>
1685
- <p className="text-sm font-medium">
1686
- {mfa.name}
1687
- </p>
1688
- <p className="text-xs text-muted-foreground">
1689
- {t('mfaEnabled')}
1690
- </p>
1691
- </div>
1692
- </div>
1693
- </div>
1694
- ))}
1695
- </div>
1696
- ) : (
1697
- <div className="rounded-lg border border-dashed p-6 text-center">
1698
- <Shield className="h-12 w-12 text-muted-foreground mx-auto mb-3" />
1699
- <p className="text-sm font-medium mb-1">
1700
- {t('noMfaEnabled')}
1701
- </p>
1702
- <p className="text-xs text-muted-foreground mb-4">
1703
- {t('enhanceSecurity')}
1704
- </p>
1705
- </div>
1706
- )}
1707
- </div>
1708
- </TabsContent>
1709
-
1710
- <TabsContent
1711
- value="sessions"
1712
- className="space-y-4 mt-4 p-4 pt-0"
1713
- >
1714
- <ActiveSessions
1715
- editingUser={editingUser}
1716
- refetch={handleRefreshEditingUser}
1717
- />
1718
- </TabsContent>
1719
- </Tabs>
1720
- </SheetContent>
1721
- </Sheet>
1722
- )}
359
+ <UserSheet
360
+ open={isSheetOpen}
361
+ onOpenChange={handleSheetClose}
362
+ userId={selectedUserId}
363
+ onSuccess={refetch}
364
+ />
1723
365
  </div>
1724
366
  </Page>
1725
367
  );