@hed-hog/core 0.0.185 → 0.0.190

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 (55) hide show
  1. package/hedhog/frontend/app/account/2fa/page.tsx.ejs +5 -0
  2. package/hedhog/frontend/app/account/accounts/page.tsx.ejs +5 -0
  3. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +356 -0
  4. package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +379 -0
  5. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +184 -0
  6. package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +144 -0
  7. package/hedhog/frontend/app/account/components/email-request-dialog.tsx.ejs +96 -0
  8. package/hedhog/frontend/app/account/components/mfa-add-buttons.tsx.ejs +43 -0
  9. package/hedhog/frontend/app/account/components/mfa-method-card.tsx.ejs +115 -0
  10. package/hedhog/frontend/app/account/components/mfa-setup-dialog.tsx.ejs +236 -0
  11. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +209 -0
  12. package/hedhog/frontend/app/account/components/recovery-codes-dialog.tsx.ejs +192 -0
  13. package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +372 -0
  14. package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +337 -0
  15. package/hedhog/frontend/app/account/components/two-factor-auth.tsx.ejs +393 -0
  16. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +332 -0
  17. package/hedhog/frontend/app/account/email/page.tsx.ejs +5 -0
  18. package/hedhog/frontend/app/account/hooks/use-mfa-methods.ts.ejs +27 -0
  19. package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +461 -0
  20. package/hedhog/frontend/app/account/layout.tsx.ejs +105 -0
  21. package/hedhog/frontend/app/account/lib/mfa-utils.tsx.ejs +37 -0
  22. package/hedhog/frontend/app/account/page.tsx.ejs +5 -0
  23. package/hedhog/frontend/app/account/password/page.tsx.ejs +5 -0
  24. package/hedhog/frontend/app/account/profile/page.tsx.ejs +5 -0
  25. package/hedhog/frontend/app/account/sessions/page.tsx.ejs +5 -0
  26. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +490 -0
  27. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +62 -0
  28. package/hedhog/frontend/app/configurations/layout.tsx.ejs +316 -0
  29. package/hedhog/frontend/app/configurations/page.tsx.ejs +35 -0
  30. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +351 -0
  31. package/hedhog/frontend/app/dashboard/[slug]/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +62 -0
  33. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +45 -0
  34. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +196 -0
  35. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +63 -0
  36. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +516 -0
  37. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +753 -0
  38. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +516 -0
  39. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +489 -0
  40. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +621 -0
  41. package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -0
  42. package/hedhog/frontend/app/mail/log/page.tsx.ejs +312 -0
  43. package/hedhog/frontend/app/mail/template/page.tsx.ejs +1177 -0
  44. package/hedhog/frontend/app/preferences/page.tsx.ejs +448 -0
  45. package/hedhog/frontend/app/roles/menus.tsx.ejs +504 -0
  46. package/hedhog/frontend/app/roles/page.tsx.ejs +814 -0
  47. package/hedhog/frontend/app/roles/routes.tsx.ejs +397 -0
  48. package/hedhog/frontend/app/roles/users.tsx.ejs +306 -0
  49. package/hedhog/frontend/app/users/active-session.tsx.ejs +159 -0
  50. package/hedhog/frontend/app/users/identifiers.tsx.ejs +279 -0
  51. package/hedhog/frontend/app/users/page.tsx.ejs +1257 -0
  52. package/hedhog/frontend/app/users/permissions.tsx.ejs +155 -0
  53. package/hedhog/frontend/messages/en.json +1080 -0
  54. package/hedhog/frontend/messages/pt.json +1135 -0
  55. package/package.json +4 -4
@@ -0,0 +1,1257 @@
1
+ 'use client';
2
+
3
+ import {
4
+ PageHeader,
5
+ PaginationFooter,
6
+ SearchBar,
7
+ StatsCards,
8
+ } from '@/components/entity-list';
9
+ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
10
+ import { Button } from '@/components/ui/button';
11
+ import {
12
+ Card,
13
+ CardContent,
14
+ CardDescription,
15
+ CardHeader,
16
+ CardTitle,
17
+ } from '@/components/ui/card';
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogDescription,
22
+ DialogHeader,
23
+ DialogTitle,
24
+ } from '@/components/ui/dialog';
25
+ import {
26
+ Form,
27
+ FormControl,
28
+ FormField,
29
+ FormItem,
30
+ FormLabel,
31
+ FormMessage,
32
+ } from '@/components/ui/form';
33
+ import { Input } from '@/components/ui/input';
34
+ import {
35
+ Sheet,
36
+ SheetContent,
37
+ SheetDescription,
38
+ SheetHeader,
39
+ SheetTitle,
40
+ } from '@/components/ui/sheet';
41
+ import { formatDateTime } from '@/lib/format-date';
42
+ import { getPhotoUrl } from '@/lib/get-photo-url';
43
+ import { getUserEmail } from '@/lib/get-user-email';
44
+ import { PaginatedResult } from '@hed-hog/api-pagination';
45
+ import { User, UserMfa } from '@hed-hog/api-types';
46
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
47
+ import { zodResolver } from '@hookform/resolvers/zod';
48
+ import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar';
49
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@radix-ui/react-tabs';
50
+ import { formatDistanceToNow } from 'date-fns';
51
+ import { enUS, ptBR } from 'date-fns/locale';
52
+ import {
53
+ Activity,
54
+ Camera,
55
+ Clock,
56
+ Eye,
57
+ EyeOff,
58
+ Fingerprint,
59
+ Key,
60
+ KeyRound,
61
+ LogOut,
62
+ LucideIcon,
63
+ Mail,
64
+ Monitor,
65
+ RefreshCcw,
66
+ Save,
67
+ Shield,
68
+ ShieldCheck,
69
+ ShieldOff,
70
+ Trash2,
71
+ UserCircle,
72
+ UserIcon,
73
+ UserPlus,
74
+ Users,
75
+ } from 'lucide-react';
76
+ import { useTranslations } from 'next-intl';
77
+ import { useEffect, useState } from 'react';
78
+ import { useForm } from 'react-hook-form';
79
+ import { toast } from 'sonner';
80
+ import { z } from 'zod';
81
+ import { ActiveSessions } from './active-session';
82
+ import { UserIdentifiersSection } from './identifiers';
83
+ import { PermissionsSection } from './permissions';
84
+
85
+ type RequestResponse<T> = {
86
+ paginate: PaginatedResult<T>;
87
+ stats: {
88
+ total: number;
89
+ newLast7Days: number;
90
+ blocked: number;
91
+ };
92
+ };
93
+
94
+ export default function UserPage() {
95
+ const t = useTranslations('core.UserPage');
96
+ const userActivityT = useTranslations('core.UserActivity');
97
+ const { request, currentLocaleCode, getSettingValue } = useApp();
98
+
99
+ const [searchQuery, setSearchQuery] = useState('');
100
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
101
+ const [showPassword, setShowPassword] = useState(false);
102
+ const [editingUser, setEditingUser] = useState<User | null>(null);
103
+ const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
104
+ const [formError, setFormError] = useState<string | null>(null);
105
+ const [editFormError, setEditFormError] = useState<string | null>(null);
106
+ const [photo, setPhoto] = useState<number | null | undefined>(null);
107
+
108
+ const [page, setPage] = useState(1);
109
+ const [pageSize, setPageSize] = useState(12);
110
+ const [statusFilter, setStatusFilter] = useState('all');
111
+
112
+ const {
113
+ data: { paginate, stats } = {
114
+ paginate: {},
115
+ stats: { total: 0, newLast7Days: 0, blocked: 0 },
116
+ },
117
+ isLoading,
118
+ refetch,
119
+ } = useQuery<RequestResponse<User>>({
120
+ queryKey: [
121
+ 'users',
122
+ page,
123
+ pageSize,
124
+ searchQuery,
125
+ statusFilter,
126
+ currentLocaleCode,
127
+ ],
128
+ queryFn: async () => {
129
+ const params = new URLSearchParams();
130
+ params.set('page', String(page));
131
+ params.set('pageSize', String(pageSize));
132
+ if (searchQuery) params.set('search', searchQuery);
133
+ if (statusFilter && statusFilter !== 'all')
134
+ params.set('filter', statusFilter);
135
+
136
+ const response = await request<RequestResponse<User>>({
137
+ url: `/user?${params.toString()}`,
138
+ method: 'GET',
139
+ });
140
+ return response.data;
141
+ },
142
+ });
143
+
144
+ const addUserSchema = z.object({
145
+ name: z.string().min(2, t('errorName')),
146
+ email: z.string().email(t('errorEmail')),
147
+ password: z
148
+ .string()
149
+ .min(6, t('errorPassword'))
150
+ .optional()
151
+ .or(z.literal('')),
152
+ });
153
+
154
+ const form = useForm<z.infer<typeof addUserSchema>>({
155
+ resolver: zodResolver(addUserSchema),
156
+ defaultValues: {
157
+ name: '',
158
+ email: '',
159
+ password: '',
160
+ },
161
+ });
162
+
163
+ const editUserSchema = z.object({
164
+ name: z.string().min(2, t('errorName')),
165
+ email: z.string().email(t('errorEmail')),
166
+ });
167
+
168
+ const editForm = useForm({
169
+ resolver: zodResolver(editUserSchema),
170
+ defaultValues: {
171
+ name: '',
172
+ email: '',
173
+ },
174
+ });
175
+
176
+ const [activeTab, setActiveTab] = useState('overview');
177
+ const [openDeleteModal, setOpenDeleteModal] = useState(false);
178
+
179
+ useEffect(() => {
180
+ if (editingUser) {
181
+ editForm.reset({
182
+ name: editingUser.name || '',
183
+ email: getUserEmail(editingUser),
184
+ });
185
+ setActiveTab('overview');
186
+ }
187
+ }, [editingUser]);
188
+
189
+ const onSubmit = async (values: z.infer<typeof addUserSchema>) => {
190
+ try {
191
+ await request({
192
+ url: '/user',
193
+ method: 'POST',
194
+ data: values,
195
+ });
196
+ form.reset();
197
+ refetch();
198
+ setIsDialogOpen(false);
199
+ setFormError(null);
200
+ } catch (err) {
201
+ const e: any = err;
202
+ const msg =
203
+ e?.response?.data?.message ||
204
+ e?.response?.data?.error ||
205
+ e?.message ||
206
+ 'Server error';
207
+ setFormError(String(msg));
208
+ }
209
+ };
210
+
211
+ const handleEdit = (user: User) => {
212
+ setEditFormError(null);
213
+ setEditingUser(user);
214
+ };
215
+
216
+ const onDelete = async () => {
217
+ try {
218
+ await request({
219
+ url: `/user`,
220
+ method: 'DELETE',
221
+ data: {
222
+ ids: [Number(editingUser?.id)],
223
+ },
224
+ });
225
+ refetch();
226
+ setOpenDeleteModal(false);
227
+ setEditingUser(null);
228
+ setEditFormError(null);
229
+ toast.success(t('userDeletedSuccess'));
230
+ } catch (err) {
231
+ const e: any = err;
232
+ const msg =
233
+ e?.response?.data?.message ||
234
+ e?.response?.data?.error ||
235
+ e?.message ||
236
+ t('serverError');
237
+ setEditFormError(String(msg));
238
+ }
239
+ };
240
+
241
+ const handleRefreshEditingUser = async () => {
242
+ const updated = await refetch();
243
+ const freshUser = updated.data?.paginate?.data.find(
244
+ (u: User) => u.id === editingUser?.id
245
+ );
246
+
247
+ if (freshUser) {
248
+ setEditingUser(freshUser);
249
+ }
250
+ };
251
+
252
+ const onEditSubmit = async (values: z.infer<typeof editUserSchema>) => {
253
+ try {
254
+ await request({
255
+ url: `/user/${editingUser?.id}`,
256
+ method: 'PATCH',
257
+ data: values,
258
+ });
259
+ refetch();
260
+ setEditFormError(null);
261
+ toast.success(t('userUpdatedSuccess'));
262
+ } catch (err) {
263
+ const e: any = err;
264
+ const msg =
265
+ e?.response?.data?.message ||
266
+ e?.response?.data?.error ||
267
+ e?.message ||
268
+ t('serverError');
269
+ setEditFormError(String(msg));
270
+ }
271
+ };
272
+
273
+ const handleAvatarClick = () => {
274
+ const input = document.createElement('input');
275
+ input.type = 'file';
276
+ input.accept = 'image/*';
277
+ input.onchange = async (e: any) => {
278
+ const file = e.target?.files?.[0];
279
+ if (file && editingUser) {
280
+ setIsUploadingAvatar(true);
281
+ try {
282
+ const formData = new FormData();
283
+ formData.append('avatar', file);
284
+
285
+ const response: any = await request({
286
+ url: `/user/${editingUser.id}/avatar`,
287
+ method: 'POST',
288
+ data: formData,
289
+ headers: {
290
+ 'Content-Type': 'multipart/form-data',
291
+ },
292
+ });
293
+
294
+ setPhoto(response.data.id);
295
+ toast.success(t('pictureUpdatedSuccess'));
296
+ refetch();
297
+ } catch (err) {
298
+ console.error(err);
299
+ } finally {
300
+ setIsUploadingAvatar(false);
301
+ }
302
+ }
303
+ };
304
+ input.click();
305
+ };
306
+
307
+ const accountProviders = [
308
+ { name: 'Google', icon: '/google.svg' },
309
+ { name: 'GitHub', icon: '/github.svg' },
310
+ { name: 'Facebook', icon: '/facebook.svg' },
311
+ ];
312
+
313
+ return (
314
+ <div className="flex flex-col h-screen px-4">
315
+ <PageHeader
316
+ breadcrumbs={[{ label: 'Home', href: '/' }, { label: t('users') }]}
317
+ actions={[
318
+ {
319
+ label: t('buttonAddUser'),
320
+ onClick: () => setIsDialogOpen(true),
321
+ variant: 'default',
322
+ },
323
+ ]}
324
+ title={t('title')}
325
+ description={t('description')}
326
+ />
327
+
328
+ <StatsCards
329
+ stats={[
330
+ {
331
+ title: t('totalUsers'),
332
+ value: String(stats?.total || 0),
333
+ icon: <Users className="h-5 w-5" />,
334
+ iconBgColor: 'bg-purple-50',
335
+ iconColor: 'text-purple-600',
336
+ },
337
+ {
338
+ title: t('totalNewUsers7Days'),
339
+ value: String(stats?.newLast7Days || 0),
340
+ icon: <UserPlus className="h-5 w-5" />,
341
+ iconBgColor: 'bg-rose-50',
342
+ iconColor: 'text-rose-600',
343
+ },
344
+ {
345
+ title: t('totalValidatedUsers'),
346
+ value: '0',
347
+ icon: <Mail className="h-5 w-5" />,
348
+ iconBgColor: 'bg-green-50',
349
+ iconColor: 'text-green-600',
350
+ },
351
+ {
352
+ title: t('totalBlockedUsers'),
353
+ value: String(stats?.blocked || 0),
354
+ icon: <ShieldCheck className="h-5 w-5" />,
355
+ iconBgColor: 'bg-amber-50',
356
+ iconColor: 'text-amber-600',
357
+ },
358
+ ]}
359
+ />
360
+
361
+ <SearchBar
362
+ searchQuery={searchQuery}
363
+ onSearchChange={setSearchQuery}
364
+ onSearch={() => refetch()}
365
+ placeholder={t('searchPlaceholder')}
366
+ filters={{
367
+ value: statusFilter,
368
+ onChange: (value) => {
369
+ setStatusFilter(value);
370
+ setPage(1);
371
+ },
372
+ placeholder: t('filterPlaceholder'),
373
+ options: [
374
+ { label: t('filterOptionAll'), value: 'all' },
375
+ { label: t('filterOptionNew'), value: 'new' },
376
+ { label: t('filterOptionBlocked'), value: 'blocked' },
377
+ ],
378
+ }}
379
+ className="mt-4"
380
+ />
381
+
382
+ <div className="flex-1 pt-4">
383
+ {isLoading && (
384
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
385
+ {Array.from({ length: 3 }).map((_, i) => (
386
+ <Card
387
+ key={`skeleton-${i}`}
388
+ className="flex flex-col justify-between gap-2 rounded-2xl border border-border/60 bg-card p-4 shadow-sm animate-pulse"
389
+ >
390
+ <CardHeader className="flex items-start justify-between gap-4 p-0">
391
+ <div className="flex items-center gap-3">
392
+ <div className="h-12 w-12 shrink-0 rounded-full bg-muted" />
393
+ <div className="space-y-2 py-1">
394
+ <div className="h-4 w-40 rounded bg-muted" />
395
+ <div className="h-3 w-32 rounded bg-muted" />
396
+ </div>
397
+ </div>
398
+ <div className="flex flex-col items-end gap-2">
399
+ <div className="h-8 w-20 rounded bg-muted" />
400
+ <div className="mt-1 h-5 w-20 rounded bg-muted" />
401
+ </div>
402
+ </CardHeader>
403
+ <div className="mt-3 space-y-2 text-sm">
404
+ <div className="h-3 w-full rounded bg-muted" />
405
+ <div className="h-3 w-full rounded bg-muted" />
406
+ <div className="h-3 w-1/2 rounded bg-muted" />
407
+ </div>
408
+ </Card>
409
+ ))}
410
+ </div>
411
+ )}
412
+
413
+ {(paginate?.data?.length || 0) === 0 ? (
414
+ <p className="text-sm text-muted-foreground">{t('noUsersFound')}</p>
415
+ ) : (
416
+ <div className="grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
417
+ {paginate?.data?.map((user: User) => (
418
+ <Card
419
+ key={user.id}
420
+ onDoubleClick={() => handleEdit(user)}
421
+ 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"
422
+ >
423
+ <CardHeader className="flex items-start justify-between gap-4 p-0">
424
+ <div className="flex items-center gap-3">
425
+ <div className="h-12 w-12 shrink-0 rounded-full bg-muted">
426
+ <img
427
+ src={getPhotoUrl(user.photo_id)}
428
+ alt={user.name}
429
+ className="h-12 w-12 rounded-full object-cover"
430
+ />
431
+ </div>
432
+ <div>
433
+ <CardTitle className="text-sm font-semibold">
434
+ {user.name || '—'}
435
+ </CardTitle>
436
+ <CardDescription className="text-xs text-muted-foreground">
437
+ {getUserEmail(user)}
438
+ </CardDescription>
439
+ </div>
440
+ </div>
441
+ <div className="flex flex-col items-end gap-2">
442
+ <Button
443
+ variant="outline"
444
+ size="sm"
445
+ onClick={() => handleEdit(user)}
446
+ >
447
+ {t('buttonEditUser')}
448
+ </Button>
449
+
450
+ <div className="mt-1">
451
+ {user.suspended_until ? (
452
+ <span className="inline-flex items-center gap-2 rounded-full bg-rose-50 px-2 py-0.5 text-xs font-medium text-rose-700">
453
+ <ShieldCheck className="h-3 w-3" />
454
+ {t('blocked')}
455
+ </span>
456
+ ) : (
457
+ <span className="inline-flex items-center gap-2 rounded-full bg-green-50 px-2 py-0.5 text-xs font-medium text-green-700">
458
+ <Users className="h-3 w-3" />
459
+ {t('active')}
460
+ </span>
461
+ )}
462
+ </div>
463
+ </div>
464
+ </CardHeader>
465
+ <div className="mt-3 space-y-2 text-sm text-muted-foreground">
466
+ <div className="flex flex-wrap items-center gap-2">
467
+ {(user as any).user_account?.includes('google') && (
468
+ <div
469
+ title={t('googleConnected')}
470
+ className="inline-flex items-center gap-2 rounded-md bg-slate-50 px-2 py-1 text-xs text-slate-700"
471
+ >
472
+ <svg
473
+ className="h-4 w-4"
474
+ viewBox="0 0 24 24"
475
+ fill="currentColor"
476
+ aria-hidden
477
+ />
478
+ {t('google')}
479
+ </div>
480
+ )}
481
+ {(user as any).connectedAccounts?.includes('github') && (
482
+ <div
483
+ title={t('githubConnected')}
484
+ className="inline-flex items-center gap-2 rounded-md bg-slate-50 px-2 py-1 text-xs text-slate-700"
485
+ >
486
+ <svg
487
+ className="h-4 w-4"
488
+ viewBox="0 0 24 24"
489
+ fill="currentColor"
490
+ aria-hidden
491
+ />
492
+ {t('github')}
493
+ </div>
494
+ )}
495
+ {(user as any).connectedAccounts?.includes('facebook') && (
496
+ <div
497
+ title={t('facebookConnected')}
498
+ className="inline-flex items-center gap-2 rounded-md bg-slate-50 px-2 py-1 text-xs text-slate-700"
499
+ >
500
+ <svg
501
+ className="h-4 w-4"
502
+ viewBox="0 0 24 24"
503
+ fill="currentColor"
504
+ aria-hidden
505
+ />
506
+ {t('facebook')}
507
+ </div>
508
+ )}
509
+ {(!(user as any).connectedAccounts ||
510
+ (user as any).connectedAccounts.length === 0) && (
511
+ <div className="text-xs text-muted-foreground">
512
+ {t('noConnectedAccounts')}
513
+ </div>
514
+ )}
515
+ </div>
516
+ <div className="flex flex-wrap items-center gap-2">
517
+ {user.user_mfa && user.user_mfa.length > 0 ? (
518
+ user.user_mfa.map((mfa: UserMfa) => (
519
+ <span
520
+ key={String(mfa.id)}
521
+ className="rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700"
522
+ title={`MFA: ${mfa}`}
523
+ >
524
+ {mfa.name}
525
+ </span>
526
+ ))
527
+ ) : (
528
+ <span className="text-xs text-muted-foreground">
529
+ {t('noMfaMethods')}
530
+ </span>
531
+ )}
532
+ </div>
533
+ {user.suspended_reason && (
534
+ <div className="text-xs text-muted-foreground">
535
+ <span className="font-medium text-foreground">
536
+ {t('reason')}:
537
+ </span>{' '}
538
+ {user.suspended_reason}
539
+ </div>
540
+ )}
541
+ {user.suspended_until && (
542
+ <div className="text-xs text-muted-foreground">
543
+ <span className="font-medium text-foreground">
544
+ {t('until')}:
545
+ </span>{' '}
546
+ {user.suspended_until}
547
+ </div>
548
+ )}
549
+ </div>
550
+ </Card>
551
+ ))}
552
+ </div>
553
+ )}
554
+
555
+ <div className="w-full border-t pt-2 mt-4">
556
+ <PaginationFooter
557
+ currentPage={page}
558
+ pageSize={pageSize}
559
+ totalItems={
560
+ (paginate as any)?.total ?? (paginate as any)?.count ?? 0
561
+ }
562
+ onPageChange={setPage}
563
+ onPageSizeChange={(size) => {
564
+ setPageSize(size);
565
+ setPage(1);
566
+ }}
567
+ pageSizeOptions={[6, 12, 24, 48]}
568
+ />
569
+ </div>
570
+
571
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
572
+ <DialogContent className="sm:max-w-lg">
573
+ <DialogHeader>
574
+ <DialogTitle>{t('dialogAddUserTitle')}</DialogTitle>
575
+ <DialogDescription>{t('description')}</DialogDescription>
576
+ </DialogHeader>
577
+ <div className="w-full border-t pt-1 mt-1" />
578
+ <Form {...form}>
579
+ <form
580
+ onSubmit={form.handleSubmit(onSubmit)}
581
+ className="space-y-4"
582
+ >
583
+ <FormField
584
+ control={form.control}
585
+ name="name"
586
+ render={({ field }) => (
587
+ <FormItem>
588
+ <FormLabel>{t('formNameLabel')}</FormLabel>
589
+ <FormControl>
590
+ <Input {...field} />
591
+ </FormControl>
592
+ <FormMessage />
593
+ </FormItem>
594
+ )}
595
+ />
596
+ <FormField
597
+ control={form.control}
598
+ name="email"
599
+ render={({ field }) => (
600
+ <FormItem>
601
+ <FormLabel>{t('formEmailLabel')}</FormLabel>
602
+ <FormControl>
603
+ <Input type="email" {...field} />
604
+ </FormControl>
605
+ <FormMessage />
606
+ </FormItem>
607
+ )}
608
+ />
609
+ <FormField
610
+ control={form.control}
611
+ name="password"
612
+ render={({ field }) => (
613
+ <FormItem>
614
+ <FormLabel>{t('formPasswordLabel')}</FormLabel>
615
+ <FormControl>
616
+ <div className="relative">
617
+ <Input
618
+ type={showPassword ? 'text' : 'password'}
619
+ {...field}
620
+ className="pr-10"
621
+ />
622
+ <button
623
+ type="button"
624
+ onClick={() => setShowPassword((s) => !s)}
625
+ 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"
626
+ aria-label={
627
+ showPassword
628
+ ? t('hidePassword')
629
+ : t('showPassword')
630
+ }
631
+ >
632
+ {showPassword ? (
633
+ <EyeOff className="h-4 w-4" />
634
+ ) : (
635
+ <Eye className="h-4 w-4" />
636
+ )}
637
+ </button>
638
+ </div>
639
+ </FormControl>
640
+ <FormMessage />
641
+ </FormItem>
642
+ )}
643
+ />
644
+
645
+ {formError && (
646
+ <Alert
647
+ variant="destructive"
648
+ className="border-red-300 bg-red-50 rounded-md p-4"
649
+ >
650
+ <AlertTitle className="text-sm">
651
+ {t('verifyYourInput')}
652
+ </AlertTitle>
653
+ <AlertDescription className="text-sm">
654
+ {formError}
655
+ </AlertDescription>
656
+ </Alert>
657
+ )}
658
+
659
+ <Button type="submit" className="w-full">
660
+ {t('buttonAddUser')}
661
+ </Button>
662
+ </form>
663
+ </Form>
664
+ </DialogContent>
665
+ </Dialog>
666
+
667
+ {editingUser && (
668
+ <Sheet
669
+ open={!!editingUser}
670
+ onOpenChange={() => {
671
+ setEditingUser(null);
672
+ setPhoto(null);
673
+ }}
674
+ >
675
+ <SheetContent className="w-full sm:max-w-4xl overflow-y-auto gap-0">
676
+ <SheetHeader>
677
+ <SheetTitle>{t('titleEditUser')}</SheetTitle>
678
+ <SheetDescription>{t('description')}</SheetDescription>
679
+ </SheetHeader>
680
+
681
+ <Tabs value={activeTab} onValueChange={setActiveTab}>
682
+ <TabsList className="grid w-full grid-cols-7 bg-muted rounded-md text-muted-foreground text-sm">
683
+ <TabsTrigger
684
+ value="overview"
685
+ 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"
686
+ >
687
+ <UserCircle className="h-4 w-4 min-h-4 min-w-4" />
688
+ <span className="hidden md:inline">{t('tabOverview')}</span>
689
+ </TabsTrigger>
690
+ <TabsTrigger
691
+ value="edit"
692
+ 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"
693
+ >
694
+ <Save className="h-4 w-4 min-h-4 min-w-4" />
695
+ <span className="hidden md:inline">{t('tabEdit')}</span>
696
+ </TabsTrigger>
697
+ <TabsTrigger
698
+ value="credentials"
699
+ 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"
700
+ >
701
+ <Key className="h-4 w-4 min-h-4 min-w-4" />
702
+ <span className="hidden md:inline">
703
+ {t('tabCredentials')}
704
+ </span>
705
+ </TabsTrigger>
706
+ <TabsTrigger
707
+ value="identifiers"
708
+ 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"
709
+ >
710
+ <Fingerprint className="h-4 w-4 min-h-4 min-w-4" />
711
+ <span className="hidden md:inline">
712
+ {t('tabIdentifiers')}
713
+ </span>
714
+ </TabsTrigger>
715
+ <TabsTrigger
716
+ value="permissions"
717
+ 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"
718
+ >
719
+ <ShieldCheck className="h-4 w-4 min-h-4 min-w-4" />
720
+ <span className="hidden md:inline">
721
+ {t('tabPermissions')}
722
+ </span>
723
+ </TabsTrigger>
724
+ <TabsTrigger
725
+ value="mfa"
726
+ 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"
727
+ >
728
+ <Shield className="h-4 w-4 min-h-4 min-w-4" />
729
+ <span className="hidden md:inline">{t('tabMfa')}</span>
730
+ </TabsTrigger>
731
+ <TabsTrigger
732
+ value="sessions"
733
+ 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"
734
+ >
735
+ <Monitor className="h-4 w-4 min-h-4 min-w-4" />
736
+ <span className="hidden md:inline">{t('tabSessions')}</span>
737
+ </TabsTrigger>
738
+ </TabsList>
739
+
740
+ <TabsContent
741
+ value="overview"
742
+ className="space-y-4 mt-4 p-4 pt-0"
743
+ >
744
+ <div className="flex flex-col items-center gap-3 py-4">
745
+ <div className="relative group">
746
+ <Avatar className="h-24 w-24 shadow-md">
747
+ <AvatarImage
748
+ src={getPhotoUrl(photo || editingUser.photo_id)}
749
+ alt={editingUser.name}
750
+ className="object-cover h-24 w-24 rounded-full"
751
+ />
752
+ <AvatarFallback className="text-2xl font-semibold bg-linear-to-br from-purple-500 to-pink-500 text-white">
753
+ {editingUser.name?.charAt(0)?.toUpperCase() || 'U'}
754
+ </AvatarFallback>
755
+ </Avatar>
756
+ <button
757
+ onClick={handleAvatarClick}
758
+ disabled={isUploadingAvatar}
759
+ 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"
760
+ >
761
+ {isUploadingAvatar ? (
762
+ <div className="h-5 w-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
763
+ ) : (
764
+ <Camera className="h-6 w-6 text-white" />
765
+ )}
766
+ </button>
767
+ <div className="absolute -bottom-1 -right-1">
768
+ {editingUser.suspended_until ? (
769
+ <div className="rounded-full bg-rose-500 p-1.5 shadow-md">
770
+ <ShieldCheck className="h-3.5 w-3.5 text-white" />
771
+ </div>
772
+ ) : (
773
+ <div className="rounded-full bg-green-500 p-1.5 shadow-md">
774
+ <Users className="h-3.5 w-3.5 text-white" />
775
+ </div>
776
+ )}
777
+ </div>
778
+ </div>
779
+ <div className="text-center space-y-0.5">
780
+ <h3 className="text-xl font-bold bg-linear-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
781
+ {editingUser.name}
782
+ </h3>
783
+ <p className="text-sm text-muted-foreground flex items-center gap-2 justify-center">
784
+ <Mail className="h-4 w-4" />
785
+ {getUserEmail(editingUser)}
786
+ </p>
787
+ </div>
788
+ </div>
789
+
790
+ <div className="grid grid-cols-2 gap-2">
791
+ <Card
792
+ className={`border-l-4 ${editingUser.suspended_until ? 'border-l-red-500' : 'border-l-green-500'} py-2`}
793
+ >
794
+ <CardHeader className="p-2">
795
+ <div className="flex items-center justify-between">
796
+ <div>
797
+ <CardDescription className="text-xs">
798
+ {t('status')}
799
+ </CardDescription>
800
+ <CardTitle className="text-sm">
801
+ {editingUser.suspended_until ? (
802
+ <span className="text-rose-600">
803
+ {t('blocked')}
804
+ </span>
805
+ ) : (
806
+ <span className="text-green-600">
807
+ {t('active')}
808
+ </span>
809
+ )}
810
+ </CardTitle>
811
+ </div>
812
+ <div
813
+ className={`rounded-full p-1 ${editingUser.suspended_until ? 'bg-rose-100' : 'bg-green-100'}`}
814
+ >
815
+ <Activity
816
+ className={`h-6 w-6 ${editingUser.suspended_until ? 'text-rose-600' : 'text-green-600'}`}
817
+ />
818
+ </div>
819
+ </div>
820
+ </CardHeader>
821
+ </Card>
822
+
823
+ <Card className="border-l-4 border-l-primary-500 py-2">
824
+ <CardHeader className="p-2">
825
+ <div className="flex items-center justify-between">
826
+ <div>
827
+ <CardDescription className="text-xs">
828
+ {t('cardLastLoginDescription')}
829
+ </CardDescription>
830
+ {Boolean(editingUser.user_session?.length) ? (
831
+ <CardTitle className="text-sm truncate">
832
+ {editingUser.user_session?.[0]?.created_at &&
833
+ formatDistanceToNow(
834
+ new Date(
835
+ editingUser.user_session?.[0]?.created_at
836
+ ),
837
+ {
838
+ addSuffix: true,
839
+ locale:
840
+ currentLocaleCode === 'en'
841
+ ? enUS
842
+ : ptBR,
843
+ }
844
+ )}
845
+ <small className="text-xs ml-2 text-muted-foreground">
846
+ {formatDateTime(
847
+ String(
848
+ editingUser.user_session?.[0]?.created_at
849
+ ),
850
+ getSettingValue,
851
+ currentLocaleCode
852
+ )}
853
+ </small>
854
+ </CardTitle>
855
+ ) : (
856
+ <CardTitle>{t('cardLastLoginTitle')}</CardTitle>
857
+ )}
858
+ </div>
859
+ <div className="rounded-full bg-primary-100 p-1">
860
+ <Clock className="h-6 w-6 text-primary-600" />
861
+ </div>
862
+ </div>
863
+ </CardHeader>
864
+ </Card>
865
+ </div>
866
+
867
+ <div className="space-y-3">
868
+ <div className="flex items-center justify-between">
869
+ <h4 className="text-sm font-semibold flex items-center gap-2">
870
+ <Shield className="h-4 w-4" />
871
+ {t('securityTitle')}
872
+ </h4>
873
+ </div>
874
+
875
+ <div className="grid grid-cols-3 gap-2">
876
+ <Card className="border-l-4 border-l-amber-500 py-2">
877
+ <CardHeader className="p-2">
878
+ <div className="flex flex-col items-center text-center">
879
+ <div className="rounded-full bg-amber-100 p-1 mb-1">
880
+ <Shield className="h-4 w-4 text-amber-600" />
881
+ </div>
882
+ <CardDescription className="text-xs">
883
+ {t('securityDescription')}
884
+ </CardDescription>
885
+ <CardTitle className="text-sm font-bold">
886
+ {editingUser.user_mfa &&
887
+ editingUser.user_mfa.length > 0 ? (
888
+ <span className="text-amber-600">
889
+ {t('securityLevelHight')}
890
+ </span>
891
+ ) : (
892
+ <span className="text-orange-600">
893
+ {t('securityLevelMedium')}
894
+ </span>
895
+ )}
896
+ </CardTitle>
897
+ </div>
898
+ </CardHeader>
899
+ </Card>
900
+
901
+ <Card className="border-l-4 border-l-purple-500 py-2">
902
+ <CardHeader className="p-2">
903
+ <div className="flex flex-col items-center text-center">
904
+ <div className="rounded-full bg-purple-100 p-1 mb-1">
905
+ <Shield className="h-4 w-4 text-purple-600" />
906
+ </div>
907
+ <CardDescription className="text-xs">
908
+ {t('cardMfaMethods')}
909
+ </CardDescription>
910
+ <CardTitle className="text-sm font-bold text-purple-600">
911
+ {editingUser.user_mfa?.length || 0}
912
+ </CardTitle>
913
+ </div>
914
+ </CardHeader>
915
+ </Card>
916
+
917
+ <Card className="border-l-4 border-l-blue-500 py-2">
918
+ <CardHeader className="p-2">
919
+ <div className="flex flex-col items-center text-center">
920
+ <div className="rounded-full bg-blue-100 p-1 mb-1">
921
+ <Fingerprint className="h-4 w-4 text-blue-600" />
922
+ </div>
923
+ <CardDescription className="text-xs">
924
+ {t('cardUserId')}
925
+ </CardDescription>
926
+ <CardTitle className="text-xs font-bold text-blue-600">
927
+ #{editingUser.id}
928
+ </CardTitle>
929
+ </div>
930
+ </CardHeader>
931
+ </Card>
932
+ </div>
933
+ </div>
934
+
935
+ {editingUser.suspended_until && (
936
+ <Card className="border-rose-200 bg-rose-50/50">
937
+ <CardHeader className="p-2">
938
+ <div className="flex items-start gap-2">
939
+ <div className="rounded-full bg-rose-100 p-1">
940
+ <ShieldCheck className="h-3 w-3 text-rose-600" />
941
+ </div>
942
+ <div className="flex-1 space-y-2">
943
+ <div>
944
+ <p className="text-sm font-semibold text-rose-900">
945
+ {t('accountSuspended')}
946
+ </p>
947
+ <p className="text-xs text-rose-700 flex items-center gap-1 mt-1">
948
+ <Clock className="h-3 w-3" />
949
+ {t('until')}: {editingUser.suspended_until}
950
+ </p>
951
+ </div>
952
+ {editingUser.suspended_reason && (
953
+ <div className="rounded-md bg-rose-100 p-2">
954
+ <p className="text-xs font-medium text-rose-900">
955
+ {t('reason')}
956
+ </p>
957
+ <p className="text-xs text-rose-700">
958
+ {editingUser.suspended_reason}
959
+ </p>
960
+ </div>
961
+ )}
962
+ </div>
963
+ </div>
964
+ </CardHeader>
965
+ </Card>
966
+ )}
967
+
968
+ <div className="space-y-3">
969
+ <div className="flex items-center justify-between">
970
+ <h4 className="text-sm font-semibold flex items-center gap-2">
971
+ <Shield className="h-4 w-4" />
972
+ {t('recentActivityTitle')}
973
+ </h4>
974
+ </div>
975
+
976
+ <Card className="rounded-md p-0">
977
+ <CardContent className="p-4 gap-2 flex flex-col">
978
+ {editingUser.user_activity?.length ? (
979
+ <div className="space-y-3">
980
+ {editingUser.user_activity
981
+ ?.slice(0, 5)
982
+ .map((activity, index) => {
983
+ const actionIcons: Record<string, LucideIcon> =
984
+ {
985
+ login: UserIcon,
986
+ forgotPassword: KeyRound,
987
+ logout: LogOut,
988
+ resetPassword: RefreshCcw,
989
+ revokeAllSessions: ShieldOff,
990
+ revokeSession: Shield,
991
+ };
992
+
993
+ const Icon =
994
+ actionIcons[activity.action] || UserIcon;
995
+
996
+ return (
997
+ <div
998
+ key={index}
999
+ className="flex items-start gap-3 border-b pb-3 last:border-none"
1000
+ >
1001
+ <div className="rounded-md bg-blue-50 p-2">
1002
+ <Icon className="w-4 h-4 text-gray-600" />
1003
+ </div>
1004
+ <div className="flex-1">
1005
+ <p className="text-sm font-medium">
1006
+ {userActivityT(activity.action)}
1007
+ </p>
1008
+ <p className="text-[11px] text-gray-500 mt-1">
1009
+ {activity.created_at &&
1010
+ formatDistanceToNow(
1011
+ new Date(activity.created_at),
1012
+ {
1013
+ addSuffix: true,
1014
+ locale:
1015
+ currentLocaleCode === 'en'
1016
+ ? enUS
1017
+ : ptBR,
1018
+ }
1019
+ )}
1020
+ </p>
1021
+ </div>
1022
+ </div>
1023
+ );
1024
+ })}
1025
+ </div>
1026
+ ) : (
1027
+ <span className="text-sm text-muted-foreground">
1028
+ {t('noRecentActivity')}
1029
+ </span>
1030
+ )}
1031
+ </CardContent>
1032
+ </Card>
1033
+ </div>
1034
+
1035
+ <div className="flex justify-end">
1036
+ <Button
1037
+ className="cursor-pointer"
1038
+ variant="destructive"
1039
+ onClick={() => setOpenDeleteModal(true)}
1040
+ >
1041
+ <Trash2 className="w-4 h-4" />
1042
+ <span>{t('buttonDeleteUser')}</span>
1043
+ </Button>
1044
+ </div>
1045
+
1046
+ <Dialog
1047
+ open={openDeleteModal}
1048
+ onOpenChange={setOpenDeleteModal}
1049
+ >
1050
+ <DialogContent className="sm:max-w-lg">
1051
+ <DialogHeader>
1052
+ <DialogTitle>{t('dialogDeleteUserTitle')}</DialogTitle>
1053
+ <DialogDescription>
1054
+ {t('dialogDeleteUserDescription')}
1055
+ </DialogDescription>
1056
+ </DialogHeader>
1057
+ <hr className="mt-4" />
1058
+ <div className="flex justify-end">
1059
+ <Button
1060
+ type="button"
1061
+ className="px-4 w-28 h-12 py-2 bg-gray-300 text-black hover:bg-gray-300 hover:text-black rounded-sm mr-2 text-md"
1062
+ onClick={() => setOpenDeleteModal(false)}
1063
+ >
1064
+ {t('deleteUserCancel')}
1065
+ </Button>
1066
+ <Button
1067
+ onClick={onDelete}
1068
+ variant="destructive"
1069
+ className="px-4 w-32 h-12 py-2 text-white hover:text-white rounded-sm text-md cursor-pointer"
1070
+ >
1071
+ {t('deleteUserConfirm')}
1072
+ </Button>
1073
+ </div>
1074
+ </DialogContent>
1075
+ </Dialog>
1076
+ </TabsContent>
1077
+
1078
+ <TabsContent value="edit" className="space-y-4 mt-4 p-4 pt-0">
1079
+ <Form {...editForm}>
1080
+ <form
1081
+ onSubmit={editForm.handleSubmit(onEditSubmit)}
1082
+ className="space-y-4"
1083
+ >
1084
+ <FormField
1085
+ control={editForm.control}
1086
+ name="name"
1087
+ render={({ field }) => (
1088
+ <FormItem>
1089
+ <FormLabel>{t('editNameLabel')}</FormLabel>
1090
+ <FormControl>
1091
+ <Input
1092
+ placeholder={t('editNamePlaceholder')}
1093
+ {...field}
1094
+ />
1095
+ </FormControl>
1096
+ <FormMessage />
1097
+ </FormItem>
1098
+ )}
1099
+ />
1100
+ <div className="flex flex-col w-full gap-2 pt-2">
1101
+ <Button type="submit" className="w-full">
1102
+ {t('saveChanges')}
1103
+ </Button>
1104
+ <Button
1105
+ className="w-full"
1106
+ type="button"
1107
+ variant="outline"
1108
+ onClick={() => setEditingUser(null)}
1109
+ >
1110
+ {t('cancel')}
1111
+ </Button>
1112
+ </div>
1113
+ </form>
1114
+ </Form>
1115
+ </TabsContent>
1116
+
1117
+ <TabsContent
1118
+ value="credentials"
1119
+ className="space-y-4 mt-4 p-4 pt-0"
1120
+ >
1121
+ <div className="space-y-3">
1122
+ <h4 className="text-sm font-medium">
1123
+ {t('connectedAccountsTitle')}
1124
+ </h4>
1125
+ <div className="space-y-2">
1126
+ {accountProviders.map((provider: any) => {
1127
+ const isConnected = editingUser.user_account?.includes(
1128
+ provider.name.toLowerCase()
1129
+ );
1130
+
1131
+ return (
1132
+ <div
1133
+ key={provider.name}
1134
+ className={`flex items-center justify-between rounded-lg border p-3 ${
1135
+ isConnected ? '' : 'border-dashed'
1136
+ }`}
1137
+ >
1138
+ <div className="flex items-center gap-3">
1139
+ <div className="rounded-md bg-slate-50 p-2">
1140
+ <img
1141
+ src={provider.icon}
1142
+ alt={`${provider.name} icon`}
1143
+ className={`h-5 w-5 ${
1144
+ isConnected ? 'opacity-100' : 'opacity-50'
1145
+ }`}
1146
+ />
1147
+ </div>
1148
+ <div>
1149
+ <p
1150
+ className={`text-sm font-medium ${
1151
+ isConnected ? '' : 'text-muted-foreground'
1152
+ }`}
1153
+ >
1154
+ {provider.name}
1155
+ </p>
1156
+ <p className="text-xs text-muted-foreground">
1157
+ {isConnected
1158
+ ? t('connected')
1159
+ : t('notConnected')}
1160
+ </p>
1161
+ </div>
1162
+ </div>
1163
+ </div>
1164
+ );
1165
+ })}
1166
+ </div>
1167
+ </div>
1168
+ </TabsContent>
1169
+
1170
+ <TabsContent
1171
+ value="identifiers"
1172
+ className="space-y-4 mt-4 p-4 pt-0"
1173
+ >
1174
+ <UserIdentifiersSection editingUser={editingUser} />
1175
+ </TabsContent>
1176
+
1177
+ <TabsContent
1178
+ value="permissions"
1179
+ className="space-y-4 mt-4 p-4 pt-0"
1180
+ >
1181
+ <div className="space-y-3">
1182
+ <div className="flex items-center justify-between">
1183
+ <div>
1184
+ <h4 className="text-sm font-semibold flex items-center gap-2">
1185
+ <ShieldCheck className="h-4 w-4" />
1186
+ {t('permissionsTitle')}
1187
+ </h4>
1188
+ <p className="text-xs text-muted-foreground mt-1">
1189
+ {t('permissionsDescription')}
1190
+ </p>
1191
+ </div>
1192
+ </div>
1193
+
1194
+ <PermissionsSection
1195
+ userId={editingUser.id!}
1196
+ onRoleChange={handleRefreshEditingUser}
1197
+ />
1198
+ </div>
1199
+ </TabsContent>
1200
+
1201
+ <TabsContent value="mfa" className="space-y-4 mt-4 p-4 pt-0">
1202
+ <div className="space-y-3">
1203
+ <h4 className="text-sm font-medium">{t('mfaTitle')}</h4>
1204
+ {editingUser.user_mfa && editingUser.user_mfa.length > 0 ? (
1205
+ <div className="space-y-2">
1206
+ {editingUser.user_mfa.map((mfa: UserMfa) => (
1207
+ <div
1208
+ key={String(mfa.id)}
1209
+ className="flex items-center justify-between rounded-lg border p-3"
1210
+ >
1211
+ <div className="flex items-center gap-3">
1212
+ <div className="rounded-md bg-amber-50 p-2">
1213
+ <Shield className="h-5 w-5 text-amber-600" />
1214
+ </div>
1215
+ <div>
1216
+ <p className="text-sm font-medium">
1217
+ {mfa.name}
1218
+ </p>
1219
+ <p className="text-xs text-muted-foreground">
1220
+ {t('mfaEnabled')}
1221
+ </p>
1222
+ </div>
1223
+ </div>
1224
+ </div>
1225
+ ))}
1226
+ </div>
1227
+ ) : (
1228
+ <div className="rounded-lg border border-dashed p-6 text-center">
1229
+ <Shield className="h-12 w-12 text-muted-foreground mx-auto mb-3" />
1230
+ <p className="text-sm font-medium mb-1">
1231
+ {t('noMfaEnabled')}
1232
+ </p>
1233
+ <p className="text-xs text-muted-foreground mb-4">
1234
+ {t('enhanceSecurity')}
1235
+ </p>
1236
+ </div>
1237
+ )}
1238
+ </div>
1239
+ </TabsContent>
1240
+
1241
+ <TabsContent
1242
+ value="sessions"
1243
+ className="space-y-4 mt-4 p-4 pt-0"
1244
+ >
1245
+ <ActiveSessions
1246
+ editingUser={editingUser}
1247
+ refetch={handleRefreshEditingUser}
1248
+ />
1249
+ </TabsContent>
1250
+ </Tabs>
1251
+ </SheetContent>
1252
+ </Sheet>
1253
+ )}
1254
+ </div>
1255
+ </div>
1256
+ );
1257
+ }