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