@hed-hog/core 0.0.276 → 0.0.279

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +60 -0
  2. package/dist/auth/auth.controller.d.ts +8 -1
  3. package/dist/auth/auth.controller.d.ts.map +1 -1
  4. package/dist/auth/auth.controller.js +7 -7
  5. package/dist/auth/auth.controller.js.map +1 -1
  6. package/dist/auth/auth.service.d.ts +10 -1
  7. package/dist/auth/auth.service.d.ts.map +1 -1
  8. package/dist/auth/auth.service.js +34 -8
  9. package/dist/auth/auth.service.js.map +1 -1
  10. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +12 -0
  11. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  12. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
  13. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  14. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +12 -0
  15. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  16. package/dist/dashboard/dashboard-core/dashboard-core.service.js +25 -0
  17. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  18. package/dist/profile/profile.service.js +1 -1
  19. package/dist/profile/profile.service.js.map +1 -1
  20. package/dist/role/guards/role.guard.d.ts +1 -0
  21. package/dist/role/guards/role.guard.d.ts.map +1 -1
  22. package/dist/role/guards/role.guard.js +18 -0
  23. package/dist/role/guards/role.guard.js.map +1 -1
  24. package/dist/session/session.service.js +1 -1
  25. package/dist/session/session.service.js.map +1 -1
  26. package/dist/user/dto/reset-password.dto.d.ts +4 -0
  27. package/dist/user/dto/reset-password.dto.d.ts.map +1 -0
  28. package/dist/user/dto/reset-password.dto.js +26 -0
  29. package/dist/user/dto/reset-password.dto.js.map +1 -0
  30. package/dist/user/user.controller.d.ts +5 -0
  31. package/dist/user/user.controller.d.ts.map +1 -1
  32. package/dist/user/user.controller.js +13 -0
  33. package/dist/user/user.controller.js.map +1 -1
  34. package/dist/user/user.service.d.ts +6 -0
  35. package/dist/user/user.service.d.ts.map +1 -1
  36. package/dist/user/user.service.js +65 -0
  37. package/dist/user/user.service.js.map +1 -1
  38. package/hedhog/data/dashboard_component.yaml +74 -12
  39. package/hedhog/data/dashboard_component_role.yaml +223 -145
  40. package/hedhog/data/dashboard_item.yaml +42 -22
  41. package/hedhog/data/dashboard_role.yaml +18 -12
  42. package/hedhog/data/menu.yaml +6 -0
  43. package/hedhog/data/route.yaml +65 -1
  44. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +2 -1
  45. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +17 -17
  46. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +23 -12
  47. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +80 -5
  48. package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +17 -13
  49. package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +16 -12
  50. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +27 -16
  51. package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +13 -9
  52. package/hedhog/frontend/app/dashboard/components/widgets/menus-card.tsx.ejs +58 -0
  53. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -58
  54. package/hedhog/frontend/app/dashboard/components/widgets/routes-card.tsx.ejs +58 -0
  55. package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +6 -6
  56. package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +6 -6
  57. package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +6 -6
  58. package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +6 -6
  59. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +15 -11
  60. package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +18 -15
  61. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +20 -4
  62. package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -14
  63. package/hedhog/frontend/app/mail/log/page.tsx.ejs +5 -11
  64. package/hedhog/frontend/app/users/page.tsx.ejs +331 -10
  65. package/hedhog/frontend/messages/en.json +29 -3
  66. package/hedhog/frontend/messages/pt.json +29 -3
  67. package/package.json +4 -4
  68. package/src/auth/auth.controller.ts +21 -20
  69. package/src/auth/auth.service.ts +63 -15
  70. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
  71. package/src/dashboard/dashboard-core/dashboard-core.service.ts +34 -0
  72. package/src/profile/profile.service.ts +1 -1
  73. package/src/role/guards/role.guard.ts +36 -7
  74. package/src/session/session.service.ts +2 -2
  75. package/src/user/dto/reset-password.dto.ts +11 -0
  76. package/src/user/user.controller.ts +24 -14
  77. package/src/user/user.service.ts +84 -0
@@ -86,29 +86,32 @@ function SessionsContent({ sessions }: { sessions: SessionData[] }) {
86
86
  const router = useRouter();
87
87
 
88
88
  return (
89
- <Card className="flex h-full flex-col">
89
+ <Card className="flex h-full min-h-0 flex-col overflow-hidden">
90
90
  <CardHeader className="shrink-0 pb-3">
91
- <div className="flex items-center justify-between">
92
- <div className="flex items-center gap-2">
93
- <Globe className="h-5 w-5 text-blue-600" />
94
- <div>
95
- <CardTitle className="text-base font-semibold">
91
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
92
+ <div className="flex min-w-0 items-center gap-2">
93
+ <Globe className="h-4 w-4 text-blue-600 sm:h-5 sm:w-5" />
94
+ <div className="min-w-0">
95
+ <CardTitle className="text-sm font-semibold sm:text-base">
96
96
  {t('title')}
97
97
  </CardTitle>
98
- <CardDescription>{t('description')}</CardDescription>
98
+ <CardDescription className="text-xs sm:text-sm">
99
+ {t('description')}
100
+ </CardDescription>
99
101
  </div>
100
102
  </div>
101
103
  <Button
102
104
  variant="outline"
103
105
  size="sm"
104
106
  onClick={() => router.push('/core/account/sessions')}
107
+ className="h-8 w-full shrink-0 gap-1.5 px-2.5 text-xs sm:h-9 sm:w-auto sm:text-sm"
105
108
  >
106
109
  <Info className="h-3.5 w-3.5" />
107
110
  {t('moreInfo')}
108
111
  </Button>
109
112
  </div>
110
113
  </CardHeader>
111
- <CardContent className="flex-1 overflow-auto pt-0">
114
+ <CardContent className="flex min-h-0 flex-1 overflow-auto pt-0">
112
115
  <div className="flex flex-col gap-2">
113
116
  {sessions.map((session, index) => {
114
117
  const ua = session.user_agent ?? '';
@@ -134,21 +137,21 @@ function SessionsContent({ sessions }: { sessions: SessionData[] }) {
134
137
  return (
135
138
  <div
136
139
  key={session.id}
137
- className={`group flex items-center gap-3 rounded-xl border p-3.5 transition-all duration-200 hover:shadow-sm ${
140
+ className={`group flex items-center gap-2.5 rounded-xl border p-3 transition-all duration-200 hover:shadow-sm sm:gap-3 sm:p-3.5 ${
138
141
  isCurrent
139
142
  ? 'border-emerald-200 bg-emerald-50/50 dark:border-emerald-800 dark:bg-emerald-950/30'
140
143
  : 'bg-card hover:bg-muted/30'
141
144
  }`}
142
145
  >
143
146
  <div
144
- className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${
147
+ className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg sm:h-10 sm:w-10 ${
145
148
  isCurrent
146
149
  ? 'bg-emerald-100 dark:bg-emerald-900/50'
147
150
  : 'bg-muted'
148
151
  }`}
149
152
  >
150
153
  <DeviceIcon
151
- className={`h-5 w-5 ${
154
+ className={`h-4 w-4 sm:h-5 sm:w-5 ${
152
155
  isCurrent
153
156
  ? 'text-emerald-600 dark:text-emerald-400'
154
157
  : 'text-muted-foreground'
@@ -156,8 +159,8 @@ function SessionsContent({ sessions }: { sessions: SessionData[] }) {
156
159
  />
157
160
  </div>
158
161
  <div className="flex min-w-0 flex-1 flex-col gap-0.5">
159
- <div className="flex items-center gap-2">
160
- <span className="text-sm font-medium text-foreground">
162
+ <div className="flex flex-wrap items-center gap-2">
163
+ <span className="min-w-0 wrap-break-word text-[13px] font-medium text-foreground sm:text-sm">
161
164
  {device}
162
165
  </span>
163
166
  {isCurrent && (
@@ -166,10 +169,10 @@ function SessionsContent({ sessions }: { sessions: SessionData[] }) {
166
169
  </Badge>
167
170
  )}
168
171
  </div>
169
- <span className="text-xs text-muted-foreground">
172
+ <span className="wrap-break-word text-[11px] text-muted-foreground sm:text-xs">
170
173
  {browser} &middot; {ip}
171
174
  </span>
172
- <div className="flex items-center gap-3 text-[11px] text-muted-foreground/70">
175
+ <div className="flex flex-wrap items-center gap-2 text-[10px] text-muted-foreground/70 sm:gap-3 sm:text-[11px]">
173
176
  <span className="flex items-center gap-1">
174
177
  <Clock className="h-3 w-3" />
175
178
  {relativeTime}
@@ -34,8 +34,12 @@
34
34
  opacity: 0.45;
35
35
  }
36
36
 
37
- .dashboard-grid .react-grid-item:hover > .react-resizable-handle.react-resizable-handle-se,
38
- .dashboard-grid .react-grid-item.resizing > .react-resizable-handle.react-resizable-handle-se {
37
+ .dashboard-grid
38
+ .react-grid-item:hover
39
+ > .react-resizable-handle.react-resizable-handle-se,
40
+ .dashboard-grid
41
+ .react-grid-item.resizing
42
+ > .react-resizable-handle.react-resizable-handle-se {
39
43
  opacity: 1;
40
44
  }
41
45
 
@@ -43,11 +47,15 @@
43
47
  display: none;
44
48
  }
45
49
 
46
- .dashboard-grid .react-grid-item > .react-resizable-handle:not(.react-resizable-handle-se) {
50
+ .dashboard-grid
51
+ .react-grid-item
52
+ > .react-resizable-handle:not(.react-resizable-handle-se) {
47
53
  display: none;
48
54
  }
49
55
 
50
- .dashboard-grid .react-grid-item > .react-resizable-handle.react-resizable-handle-se {
56
+ .dashboard-grid
57
+ .react-grid-item
58
+ > .react-resizable-handle.react-resizable-handle-se {
51
59
  bottom: 0;
52
60
  right: 0;
53
61
  cursor: se-resize;
@@ -102,3 +110,11 @@
102
110
  .dashboard-widget > [data-slot='card'] > [data-slot='card-header'] {
103
111
  padding-top: 0;
104
112
  }
113
+
114
+ @media (max-width: 639px) {
115
+ .dashboard-widget > [data-slot='card'] {
116
+ gap: 0.625rem;
117
+ padding-top: 0.625rem;
118
+ padding-bottom: 0.625rem;
119
+ }
120
+ }
@@ -1,14 +1,29 @@
1
- 'use client';
2
-
3
- import { useRouter } from 'next/navigation';
4
- import { useEffect } from 'react';
5
-
6
- export default function DashboardRedirectPage() {
7
- const router = useRouter();
8
-
9
- useEffect(() => {
10
- router.replace('/core/dashboard/default');
11
- }, [router]);
12
-
13
- return null;
14
- }
1
+ 'use client';
2
+
3
+ import { Dashboard } from '@hed-hog/api-types';
4
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
5
+ import { useRouter } from 'next/navigation';
6
+ import { useEffect } from 'react';
7
+
8
+ export default function DashboardRedirectPage() {
9
+ const router = useRouter();
10
+ const { request, currentLocaleCode } = useApp();
11
+
12
+ const { data: dashboardData, isLoading } = useQuery<Dashboard | null>({
13
+ queryKey: ['dashboard-home-redirect', currentLocaleCode],
14
+ queryFn: async () => {
15
+ const response = await request<Dashboard | null>({
16
+ url: '/dashboard-core/home',
17
+ });
18
+ return response.data;
19
+ },
20
+ });
21
+
22
+ useEffect(() => {
23
+ if (isLoading) return;
24
+
25
+ router.replace(`/core/dashboard/${dashboardData?.slug ?? 'default'}`);
26
+ }, [dashboardData?.slug, isLoading, router]);
27
+
28
+ return null;
29
+ }
@@ -51,10 +51,9 @@ export default function MailLogPage() {
51
51
  const [pageSize, setPageSize] = useState(10);
52
52
  const { request, getSettingValue } = useApp();
53
53
 
54
- const {
55
- data: { data, total },
56
- refetch: refetchLogs,
57
- } = useQuery<PaginationResult<MailSent>>({
54
+ const { data: logsResult, refetch: refetchLogs } = useQuery<
55
+ PaginationResult<MailSent>
56
+ >({
58
57
  queryKey: ['mail-sent', debouncedSearch, page, pageSize],
59
58
  queryFn: async () => {
60
59
  const response = await request({
@@ -67,13 +66,8 @@ export default function MailLogPage() {
67
66
  });
68
67
  return response.data as PaginationResult<MailSent>;
69
68
  },
70
- initialData: {
71
- data: [],
72
- total: 0,
73
- page: 1,
74
- pageSize: 10,
75
- },
76
- });
69
+ });
70
+ const { data = [], total = 0 } = logsResult ?? {};
77
71
 
78
72
  useEffect(() => {
79
73
  if (data) {
@@ -7,6 +7,16 @@ import {
7
7
  StatsCards,
8
8
  } from '@/components/entity-list';
9
9
  import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
10
+ import {
11
+ AlertDialog,
12
+ AlertDialogAction,
13
+ AlertDialogCancel,
14
+ AlertDialogContent,
15
+ AlertDialogDescription,
16
+ AlertDialogFooter,
17
+ AlertDialogHeader,
18
+ AlertDialogTitle,
19
+ } from '@/components/ui/alert-dialog';
10
20
  import { Button } from '@/components/ui/button';
11
21
  import {
12
22
  Card,
@@ -95,6 +105,7 @@ export default function UserPage() {
95
105
  const t = useTranslations('core.UserPage');
96
106
  const userActivityT = useTranslations('core.UserActivity');
97
107
  const { request, currentLocaleCode, getSettingValue } = useApp();
108
+ const minPasswordLength = Number(getSettingValue('password-min-length')) || 6;
98
109
 
99
110
  const [searchQuery, setSearchQuery] = useState('');
100
111
  const [isDialogOpen, setIsDialogOpen] = useState(false);
@@ -103,6 +114,14 @@ export default function UserPage() {
103
114
  const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
104
115
  const [formError, setFormError] = useState<string | null>(null);
105
116
  const [editFormError, setEditFormError] = useState<string | null>(null);
117
+ const [resetFormError, setResetFormError] = useState<string | null>(null);
118
+ const [isResetPasswordDialogOpen, setIsResetPasswordDialogOpen] =
119
+ useState(false);
120
+ const [isResetResultDialogOpen, setIsResetResultDialogOpen] = useState(false);
121
+ const [isResettingPassword, setIsResettingPassword] = useState(false);
122
+ const [showResetPassword, setShowResetPassword] = useState(false);
123
+ const [showResetPasswordResult, setShowResetPasswordResult] = useState(false);
124
+ const [resetPasswordResult, setResetPasswordResult] = useState('');
106
125
  const [photo, setPhoto] = useState<number | null | undefined>(null);
107
126
 
108
127
  const [page, setPage] = useState(1);
@@ -173,9 +192,57 @@ export default function UserPage() {
173
192
  },
174
193
  });
175
194
 
195
+ const resetPasswordSchema = z.object({
196
+ password: z.string().min(minPasswordLength, t('errorPassword')),
197
+ });
198
+
199
+ const resetPasswordForm = useForm<z.infer<typeof resetPasswordSchema>>({
200
+ resolver: zodResolver(resetPasswordSchema),
201
+ defaultValues: {
202
+ password: '',
203
+ },
204
+ });
205
+
176
206
  const [activeTab, setActiveTab] = useState('overview');
177
207
  const [openDeleteModal, setOpenDeleteModal] = useState(false);
178
208
 
209
+ const generateRandomPassword = (length = 16) => {
210
+ const lowercase = 'abcdefghijkmnopqrstuvwxyz';
211
+ const uppercase = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
212
+ const numbers = '23456789';
213
+ const symbols = '@#$%&*!?-_+';
214
+ const allChars = `${lowercase}${uppercase}${numbers}${symbols}`;
215
+
216
+ const groups = [lowercase, uppercase, numbers, symbols];
217
+ const randomValues = new Uint32Array(length + groups.length);
218
+ globalThis.crypto.getRandomValues(randomValues);
219
+
220
+ const chars: string[] = [];
221
+
222
+ groups.forEach((group, index) => {
223
+ chars.push(group[randomValues[index] % group.length]);
224
+ });
225
+
226
+ for (let i = groups.length; i < randomValues.length; i++) {
227
+ chars.push(allChars[randomValues[i] % allChars.length]);
228
+ }
229
+
230
+ for (let i = chars.length - 1; i > 0; i--) {
231
+ const j = randomValues[i] % (i + 1);
232
+ [chars[i], chars[j]] = [chars[j], chars[i]];
233
+ }
234
+
235
+ return chars.slice(0, length).join('');
236
+ };
237
+
238
+ const openResetPasswordDialog = () => {
239
+ const generatedPassword = generateRandomPassword();
240
+ resetPasswordForm.reset({ password: generatedPassword });
241
+ setResetFormError(null);
242
+ setShowResetPassword(false);
243
+ setIsResetPasswordDialogOpen(true);
244
+ };
245
+
179
246
  useEffect(() => {
180
247
  if (editingUser) {
181
248
  editForm.reset({
@@ -270,6 +337,50 @@ export default function UserPage() {
270
337
  }
271
338
  };
272
339
 
340
+ const onResetPasswordSubmit = async () => {
341
+ if (!editingUser?.id) {
342
+ return;
343
+ }
344
+
345
+ setIsResettingPassword(true);
346
+ setResetFormError(null);
347
+ try {
348
+ const { password } = resetPasswordForm.getValues();
349
+ const response = await request<{ password: string }>({
350
+ url: `/user/${editingUser.id}/reset-password`,
351
+ method: 'PATCH',
352
+ data: {
353
+ password,
354
+ },
355
+ });
356
+
357
+ setResetPasswordResult(response.data.password);
358
+ setIsResetPasswordDialogOpen(false);
359
+ setIsResetResultDialogOpen(true);
360
+ refetch();
361
+ toast.success(t('passwordResetSuccess'));
362
+ } catch (err) {
363
+ const e: any = err;
364
+ const msg =
365
+ e?.response?.data?.message ||
366
+ e?.response?.data?.error ||
367
+ e?.message ||
368
+ t('serverError');
369
+ setResetFormError(String(msg));
370
+ } finally {
371
+ setIsResettingPassword(false);
372
+ }
373
+ };
374
+
375
+ const copyResetPassword = async () => {
376
+ try {
377
+ await navigator.clipboard.writeText(resetPasswordResult);
378
+ toast.success(t('passwordCopied'));
379
+ } catch {
380
+ toast.error(t('passwordCopyError'));
381
+ }
382
+ };
383
+
273
384
  const handleAvatarClick = () => {
274
385
  const input = document.createElement('input');
275
386
  input.type = 'file';
@@ -568,17 +679,17 @@ export default function UserPage() {
568
679
  />
569
680
  </div>
570
681
 
571
- <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
572
- <DialogContent className="sm:max-w-lg">
573
- <DialogHeader>
574
- <DialogTitle>{t('dialogAddUserTitle')}</DialogTitle>
575
- <DialogDescription>{t('description')}</DialogDescription>
576
- </DialogHeader>
682
+ <Sheet open={isDialogOpen} onOpenChange={setIsDialogOpen}>
683
+ <SheetContent className="w-full sm:max-w-lg overflow-y-auto gap-0">
684
+ <SheetHeader>
685
+ <SheetTitle>{t('dialogAddUserTitle')}</SheetTitle>
686
+ <SheetDescription>{t('description')}</SheetDescription>
687
+ </SheetHeader>
577
688
  <div className="w-full border-t pt-1 mt-1" />
578
689
  <Form {...form}>
579
690
  <form
580
691
  onSubmit={form.handleSubmit(onSubmit)}
581
- className="space-y-4"
692
+ className="px-4 gap-4 w-full flex flex-col pt-2"
582
693
  >
583
694
  <FormField
584
695
  control={form.control}
@@ -661,15 +772,18 @@ export default function UserPage() {
661
772
  </Button>
662
773
  </form>
663
774
  </Form>
664
- </DialogContent>
665
- </Dialog>
775
+ </SheetContent>
776
+ </Sheet>
666
777
 
667
778
  {editingUser && (
668
779
  <Sheet
669
780
  open={!!editingUser}
670
- onOpenChange={() => {
781
+ onOpenChange={(open) => {
671
782
  setEditingUser(null);
672
783
  setPhoto(null);
784
+ setIsResetPasswordDialogOpen(false);
785
+ setIsResetResultDialogOpen(false);
786
+ setResetPasswordResult('');
673
787
  }}
674
788
  >
675
789
  <SheetContent className="w-full sm:max-w-4xl overflow-y-auto gap-0">
@@ -1118,6 +1232,28 @@ export default function UserPage() {
1118
1232
  value="credentials"
1119
1233
  className="space-y-4 mt-4 p-4 pt-0"
1120
1234
  >
1235
+ <Card className="border-amber-200 bg-amber-50/50">
1236
+ <CardHeader className="pb-2">
1237
+ <CardTitle className="text-sm">
1238
+ {t('passwordResetTitle')}
1239
+ </CardTitle>
1240
+ <CardDescription>
1241
+ {t('passwordResetDescription')}
1242
+ </CardDescription>
1243
+ </CardHeader>
1244
+ <CardContent className="flex flex-col gap-3">
1245
+ <div className="text-xs text-muted-foreground">
1246
+ {t('passwordResetNotice')}
1247
+ </div>
1248
+ <div>
1249
+ <Button type="button" onClick={openResetPasswordDialog}>
1250
+ <RefreshCcw className="h-4 w-4 mr-2" />
1251
+ {t('buttonResetPassword')}
1252
+ </Button>
1253
+ </div>
1254
+ </CardContent>
1255
+ </Card>
1256
+
1121
1257
  <div className="space-y-3">
1122
1258
  <h4 className="text-sm font-medium">
1123
1259
  {t('connectedAccountsTitle')}
@@ -1165,6 +1301,191 @@ export default function UserPage() {
1165
1301
  })}
1166
1302
  </div>
1167
1303
  </div>
1304
+
1305
+ <AlertDialog
1306
+ open={isResetPasswordDialogOpen}
1307
+ onOpenChange={(open) => {
1308
+ setIsResetPasswordDialogOpen(open);
1309
+ if (!open) {
1310
+ setResetFormError(null);
1311
+ }
1312
+ }}
1313
+ >
1314
+ <AlertDialogContent>
1315
+ <AlertDialogHeader>
1316
+ <AlertDialogTitle>
1317
+ {t('passwordResetDialogTitle')}
1318
+ </AlertDialogTitle>
1319
+ <AlertDialogDescription>
1320
+ {t('passwordResetDialogDescription')}
1321
+ </AlertDialogDescription>
1322
+ </AlertDialogHeader>
1323
+
1324
+ <Form {...resetPasswordForm}>
1325
+ <form className="space-y-4">
1326
+ <FormField
1327
+ control={resetPasswordForm.control}
1328
+ name="password"
1329
+ render={({ field }) => (
1330
+ <FormItem>
1331
+ <FormLabel>
1332
+ {t('passwordResetFieldLabel')}
1333
+ </FormLabel>
1334
+ <FormControl>
1335
+ <div className="space-y-2">
1336
+ <div className="relative">
1337
+ <Input
1338
+ type={
1339
+ showResetPassword
1340
+ ? 'text'
1341
+ : 'password'
1342
+ }
1343
+ {...field}
1344
+ className="pr-10"
1345
+ />
1346
+ <button
1347
+ type="button"
1348
+ onClick={() =>
1349
+ setShowResetPassword(
1350
+ (state) => !state
1351
+ )
1352
+ }
1353
+ 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"
1354
+ aria-label={
1355
+ showResetPassword
1356
+ ? t('hidePassword')
1357
+ : t('showPassword')
1358
+ }
1359
+ >
1360
+ {showResetPassword ? (
1361
+ <EyeOff className="h-4 w-4" />
1362
+ ) : (
1363
+ <Eye className="h-4 w-4" />
1364
+ )}
1365
+ </button>
1366
+ </div>
1367
+
1368
+ <Button
1369
+ type="button"
1370
+ variant="outline"
1371
+ onClick={() => {
1372
+ resetPasswordForm.setValue(
1373
+ 'password',
1374
+ generateRandomPassword(),
1375
+ {
1376
+ shouldValidate: true,
1377
+ shouldDirty: true,
1378
+ }
1379
+ );
1380
+ }}
1381
+ >
1382
+ <RefreshCcw className="h-4 w-4 mr-2" />
1383
+ {t('buttonRegeneratePassword')}
1384
+ </Button>
1385
+ </div>
1386
+ </FormControl>
1387
+ <FormMessage />
1388
+ </FormItem>
1389
+ )}
1390
+ />
1391
+
1392
+ {resetFormError && (
1393
+ <Alert variant="destructive">
1394
+ <AlertTitle>{t('verifyYourInput')}</AlertTitle>
1395
+ <AlertDescription>
1396
+ {resetFormError}
1397
+ </AlertDescription>
1398
+ </Alert>
1399
+ )}
1400
+ </form>
1401
+ </Form>
1402
+
1403
+ <AlertDialogFooter>
1404
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
1405
+ <AlertDialogAction
1406
+ disabled={isResettingPassword}
1407
+ onClick={async (event) => {
1408
+ event.preventDefault();
1409
+ const valid = await resetPasswordForm.trigger();
1410
+ if (!valid) {
1411
+ return;
1412
+ }
1413
+
1414
+ await onResetPasswordSubmit();
1415
+ }}
1416
+ >
1417
+ {isResettingPassword
1418
+ ? t('passwordResetSubmitting')
1419
+ : t('passwordResetConfirm')}
1420
+ </AlertDialogAction>
1421
+ </AlertDialogFooter>
1422
+ </AlertDialogContent>
1423
+ </AlertDialog>
1424
+
1425
+ <Dialog
1426
+ open={isResetResultDialogOpen}
1427
+ onOpenChange={(open) => {
1428
+ setIsResetResultDialogOpen(open);
1429
+ if (!open) {
1430
+ setResetPasswordResult('');
1431
+ setShowResetPasswordResult(false);
1432
+ }
1433
+ }}
1434
+ >
1435
+ <DialogContent className="sm:max-w-lg">
1436
+ <DialogHeader>
1437
+ <DialogTitle>{t('passwordResultTitle')}</DialogTitle>
1438
+ <DialogDescription>
1439
+ {t('passwordResultDescription')}
1440
+ </DialogDescription>
1441
+ </DialogHeader>
1442
+
1443
+ <div className="space-y-3">
1444
+ <div className="relative">
1445
+ <Input
1446
+ readOnly
1447
+ value={resetPasswordResult}
1448
+ type={showResetPasswordResult ? 'text' : 'password'}
1449
+ className="pr-10"
1450
+ />
1451
+ <button
1452
+ type="button"
1453
+ onClick={() =>
1454
+ setShowResetPasswordResult((state) => !state)
1455
+ }
1456
+ 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"
1457
+ aria-label={
1458
+ showResetPasswordResult
1459
+ ? t('hidePassword')
1460
+ : t('showPassword')
1461
+ }
1462
+ >
1463
+ {showResetPasswordResult ? (
1464
+ <EyeOff className="h-4 w-4" />
1465
+ ) : (
1466
+ <Eye className="h-4 w-4" />
1467
+ )}
1468
+ </button>
1469
+ </div>
1470
+
1471
+ <div className="flex justify-end gap-2">
1472
+ <Button
1473
+ type="button"
1474
+ variant="outline"
1475
+ onClick={copyResetPassword}
1476
+ >
1477
+ {t('buttonCopyPassword')}
1478
+ </Button>
1479
+ <Button
1480
+ type="button"
1481
+ onClick={() => setIsResetResultDialogOpen(false)}
1482
+ >
1483
+ {t('close')}
1484
+ </Button>
1485
+ </div>
1486
+ </div>
1487
+ </DialogContent>
1488
+ </Dialog>
1168
1489
  </TabsContent>
1169
1490
 
1170
1491
  <TabsContent