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