@hed-hog/core 0.0.276 → 0.0.278

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 (59) hide show
  1. package/dist/auth/auth.controller.d.ts +8 -1
  2. package/dist/auth/auth.controller.d.ts.map +1 -1
  3. package/dist/auth/auth.controller.js +7 -7
  4. package/dist/auth/auth.controller.js.map +1 -1
  5. package/dist/auth/auth.service.d.ts +10 -1
  6. package/dist/auth/auth.service.d.ts.map +1 -1
  7. package/dist/auth/auth.service.js +34 -8
  8. package/dist/auth/auth.service.js.map +1 -1
  9. package/dist/profile/profile.service.js +1 -1
  10. package/dist/profile/profile.service.js.map +1 -1
  11. package/dist/role/guards/role.guard.d.ts +1 -0
  12. package/dist/role/guards/role.guard.d.ts.map +1 -1
  13. package/dist/role/guards/role.guard.js +18 -0
  14. package/dist/role/guards/role.guard.js.map +1 -1
  15. package/dist/session/session.service.js +1 -1
  16. package/dist/session/session.service.js.map +1 -1
  17. package/dist/user/dto/reset-password.dto.d.ts +4 -0
  18. package/dist/user/dto/reset-password.dto.d.ts.map +1 -0
  19. package/dist/user/dto/reset-password.dto.js +26 -0
  20. package/dist/user/dto/reset-password.dto.js.map +1 -0
  21. package/dist/user/user.controller.d.ts +5 -0
  22. package/dist/user/user.controller.d.ts.map +1 -1
  23. package/dist/user/user.controller.js +13 -0
  24. package/dist/user/user.controller.js.map +1 -1
  25. package/dist/user/user.service.d.ts +6 -0
  26. package/dist/user/user.service.d.ts.map +1 -1
  27. package/dist/user/user.service.js +65 -0
  28. package/dist/user/user.service.js.map +1 -1
  29. package/hedhog/data/dashboard_component.yaml +77 -33
  30. package/hedhog/data/dashboard_component_role.yaml +132 -66
  31. package/hedhog/data/dashboard_item.yaml +100 -100
  32. package/hedhog/data/dashboard_role.yaml +18 -12
  33. package/hedhog/data/menu.yaml +6 -0
  34. package/hedhog/data/route.yaml +57 -1
  35. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +2 -1
  36. package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +24 -24
  37. package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +4 -4
  38. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +23 -19
  39. package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +15 -14
  40. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -58
  41. package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +18 -18
  42. package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +18 -18
  43. package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +18 -18
  44. package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +18 -18
  45. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +3 -3
  46. package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +34 -33
  47. package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -14
  48. package/hedhog/frontend/app/users/page.tsx.ejs +322 -1
  49. package/hedhog/frontend/messages/en.json +19 -1
  50. package/hedhog/frontend/messages/pt.json +19 -1
  51. package/package.json +4 -4
  52. package/src/auth/auth.controller.ts +21 -20
  53. package/src/auth/auth.service.ts +63 -15
  54. package/src/profile/profile.service.ts +1 -1
  55. package/src/role/guards/role.guard.ts +36 -7
  56. package/src/session/session.service.ts +2 -2
  57. package/src/user/dto/reset-password.dto.ts +11 -0
  58. package/src/user/user.controller.ts +24 -14
  59. package/src/user/user.service.ts +84 -0
@@ -34,24 +34,24 @@ export default function StatActionsToday({
34
34
  widgetName={widget?.name ?? 'stat-actions-today'}
35
35
  onRemove={onRemove}
36
36
  >
37
- <Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
38
- <CardContent className="flex h-full items-center gap-4 p-4">
39
- <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-indigo-50 dark:bg-indigo-950/40">
40
- <MousePointerClick className="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
41
- </div>
42
- <div className="flex min-w-0 flex-col">
43
- <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
44
- {t('actionsToday')}
45
- </span>
46
- <span className="text-2xl font-bold tracking-tight text-foreground">
47
- {data ?? '—'}
48
- </span>
49
- <span className="text-[11px] text-muted-foreground">
50
- {t('actionsTodaySubtitle')}
51
- </span>
52
- </div>
53
- </CardContent>
54
- </Card>
37
+ <Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
38
+ <CardContent className="flex h-full items-center gap-3 p-3 md:gap-4 md:p-4">
39
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-indigo-50 dark:bg-indigo-950/40 md:h-11 md:w-11">
40
+ <MousePointerClick className="h-4 w-4 text-indigo-600 dark:text-indigo-400 md:h-5 md:w-5" />
41
+ </div>
42
+ <div className="flex min-w-0 flex-col">
43
+ <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
44
+ {t('actionsToday')}
45
+ </span>
46
+ <span className="truncate text-xl font-bold tracking-tight text-foreground md:text-2xl">
47
+ {data ?? '—'}
48
+ </span>
49
+ <span className="hidden text-[11px] text-muted-foreground sm:block">
50
+ {t('actionsTodaySubtitle')}
51
+ </span>
52
+ </div>
53
+ </CardContent>
54
+ </Card>
55
55
  </WidgetWrapper>
56
56
  );
57
57
  }
@@ -34,24 +34,24 @@ export default function StatConsecutiveDays({
34
34
  widgetName={widget?.name ?? 'stat-consecutive-days'}
35
35
  onRemove={onRemove}
36
36
  >
37
- <Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
38
- <CardContent className="flex h-full items-center gap-4 p-4">
39
- <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-emerald-50 dark:bg-emerald-950/40">
40
- <CalendarDays className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
41
- </div>
42
- <div className="flex min-w-0 flex-col">
43
- <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
44
- {t('consecutiveDays')}
45
- </span>
46
- <span className="text-2xl font-bold tracking-tight text-foreground">
47
- {data ?? '—'}
48
- </span>
49
- <span className="text-[11px] text-muted-foreground">
50
- {t('consecutiveDaysSubtitle')}
51
- </span>
52
- </div>
53
- </CardContent>
54
- </Card>
37
+ <Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
38
+ <CardContent className="flex h-full items-center gap-3 p-3 md:gap-4 md:p-4">
39
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-emerald-50 dark:bg-emerald-950/40 md:h-11 md:w-11">
40
+ <CalendarDays className="h-4 w-4 text-emerald-600 dark:text-emerald-400 md:h-5 md:w-5" />
41
+ </div>
42
+ <div className="flex min-w-0 flex-col">
43
+ <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
44
+ {t('consecutiveDays')}
45
+ </span>
46
+ <span className="truncate text-xl font-bold tracking-tight text-foreground md:text-2xl">
47
+ {data ?? '—'}
48
+ </span>
49
+ <span className="hidden text-[11px] text-muted-foreground sm:block">
50
+ {t('consecutiveDaysSubtitle')}
51
+ </span>
52
+ </div>
53
+ </CardContent>
54
+ </Card>
55
55
  </WidgetWrapper>
56
56
  );
57
57
  }
@@ -34,24 +34,24 @@ export default function StatOnlineTime({
34
34
  widgetName={widget?.name ?? 'stat-online-time'}
35
35
  onRemove={onRemove}
36
36
  >
37
- <Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
38
- <CardContent className="flex h-full items-center gap-4 p-4">
39
- <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-blue-50 dark:bg-blue-950/40">
40
- <Clock className="h-5 w-5 text-blue-600 dark:text-blue-400" />
41
- </div>
42
- <div className="flex min-w-0 flex-col">
43
- <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
44
- {t('onlineTime')}
45
- </span>
46
- <span className="text-2xl font-bold tracking-tight text-foreground">
47
- {data ?? '—'}
48
- </span>
49
- <span className="text-[11px] text-muted-foreground">
50
- {t('onlineTimeSubtitle')}
51
- </span>
52
- </div>
53
- </CardContent>
54
- </Card>
37
+ <Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
38
+ <CardContent className="flex h-full items-center gap-3 p-3 md:gap-4 md:p-4">
39
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-blue-50 dark:bg-blue-950/40 md:h-11 md:w-11">
40
+ <Clock className="h-4 w-4 text-blue-600 dark:text-blue-400 md:h-5 md:w-5" />
41
+ </div>
42
+ <div className="flex min-w-0 flex-col">
43
+ <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
44
+ {t('onlineTime')}
45
+ </span>
46
+ <span className="truncate text-xl font-bold tracking-tight text-foreground md:text-2xl">
47
+ {data ?? '—'}
48
+ </span>
49
+ <span className="hidden text-[11px] text-muted-foreground sm:block">
50
+ {t('onlineTimeSubtitle')}
51
+ </span>
52
+ </div>
53
+ </CardContent>
54
+ </Card>
55
55
  </WidgetWrapper>
56
56
  );
57
57
  }
@@ -52,7 +52,7 @@ function RolesContent({ roles }: { roles: RoleData[] }) {
52
52
  const t = useTranslations('core.DashboardPage.userRoles');
53
53
 
54
54
  return (
55
- <Card className="flex h-full flex-col">
55
+ <Card className="flex h-full min-h-0 flex-col overflow-hidden">
56
56
  <CardHeader className="shrink-0 pb-3">
57
57
  <div className="flex items-center gap-2">
58
58
  <Crown className="h-5 w-5 text-amber-600 dark:text-amber-400" />
@@ -64,8 +64,8 @@ function RolesContent({ roles }: { roles: RoleData[] }) {
64
64
  </div>
65
65
  </div>
66
66
  </CardHeader>
67
- <CardContent className="flex-1 overflow-auto pt-0">
68
- <div className="grid grid-cols-2 gap-2">
67
+ <CardContent className="flex min-h-0 flex-1 overflow-auto pt-0">
68
+ <div className="grid grid-cols-1 gap-2 md:grid-cols-2">
69
69
  {roles.map((role, index) => {
70
70
  const style = levelStyles[index % levelStyles.length]!;
71
71
  return (
@@ -86,30 +86,31 @@ function SessionsContent({ sessions }: { sessions: SessionData[] }) {
86
86
  const router = useRouter();
87
87
 
88
88
  return (
89
- <Card className="flex h-full flex-col">
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">
96
- {t('title')}
97
- </CardTitle>
98
- <CardDescription>{t('description')}</CardDescription>
99
- </div>
100
- </div>
101
- <Button
102
- variant="outline"
103
- size="sm"
104
- onClick={() => router.push('/core/account/sessions')}
105
- >
106
- <Info className="h-3.5 w-3.5" />
107
- {t('moreInfo')}
108
- </Button>
109
- </div>
110
- </CardHeader>
111
- <CardContent className="flex-1 overflow-auto pt-0">
112
- <div className="flex flex-col gap-2">
89
+ <Card className="flex h-full min-h-0 flex-col overflow-hidden">
90
+ <CardHeader className="shrink-0 pb-3">
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-5 w-5 text-blue-600" />
94
+ <div className="min-w-0">
95
+ <CardTitle className="text-base font-semibold">
96
+ {t('title')}
97
+ </CardTitle>
98
+ <CardDescription>{t('description')}</CardDescription>
99
+ </div>
100
+ </div>
101
+ <Button
102
+ variant="outline"
103
+ size="sm"
104
+ onClick={() => router.push('/core/account/sessions')}
105
+ className="w-full shrink-0 sm:w-auto"
106
+ >
107
+ <Info className="h-3.5 w-3.5" />
108
+ {t('moreInfo')}
109
+ </Button>
110
+ </div>
111
+ </CardHeader>
112
+ <CardContent className="flex min-h-0 flex-1 overflow-auto pt-0">
113
+ <div className="flex flex-col gap-2">
113
114
  {sessions.map((session, index) => {
114
115
  const ua = session.user_agent ?? '';
115
116
  const deviceType = detectDeviceType(ua);
@@ -155,21 +156,21 @@ function SessionsContent({ sessions }: { sessions: SessionData[] }) {
155
156
  }`}
156
157
  />
157
158
  </div>
158
- <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">
161
- {device}
162
- </span>
159
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
160
+ <div className="flex flex-wrap items-center gap-2">
161
+ <span className="min-w-0 break-words text-sm font-medium text-foreground">
162
+ {device}
163
+ </span>
163
164
  {isCurrent && (
164
165
  <Badge className="bg-emerald-100 text-[10px] text-emerald-700 hover:bg-emerald-100 dark:bg-emerald-900/50 dark:text-emerald-400 dark:hover:bg-emerald-900/50">
165
166
  {t('thisSession')}
166
167
  </Badge>
167
168
  )}
168
169
  </div>
169
- <span className="text-xs text-muted-foreground">
170
- {browser} &middot; {ip}
171
- </span>
172
- <div className="flex items-center gap-3 text-[11px] text-muted-foreground/70">
170
+ <span className="break-words text-xs text-muted-foreground">
171
+ {browser} &middot; {ip}
172
+ </span>
173
+ <div className="flex flex-wrap items-center gap-3 text-[11px] text-muted-foreground/70">
173
174
  <span className="flex items-center gap-1">
174
175
  <Clock className="h-3 w-3" />
175
176
  {relativeTime}
@@ -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
+ }
@@ -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';
@@ -667,9 +778,12 @@ export default function UserPage() {
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