@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.
- package/dist/auth/auth.controller.d.ts +8 -1
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +7 -7
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +10 -1
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +34 -8
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/profile/profile.service.js +1 -1
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/role/guards/role.guard.d.ts +1 -0
- package/dist/role/guards/role.guard.d.ts.map +1 -1
- package/dist/role/guards/role.guard.js +18 -0
- package/dist/role/guards/role.guard.js.map +1 -1
- package/dist/session/session.service.js +1 -1
- package/dist/session/session.service.js.map +1 -1
- package/dist/user/dto/reset-password.dto.d.ts +4 -0
- package/dist/user/dto/reset-password.dto.d.ts.map +1 -0
- package/dist/user/dto/reset-password.dto.js +26 -0
- package/dist/user/dto/reset-password.dto.js.map +1 -0
- package/dist/user/user.controller.d.ts +5 -0
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.controller.js +13 -0
- package/dist/user/user.controller.js.map +1 -1
- package/dist/user/user.service.d.ts +6 -0
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +65 -0
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/dashboard_component.yaml +77 -33
- package/hedhog/data/dashboard_component_role.yaml +132 -66
- package/hedhog/data/dashboard_item.yaml +100 -100
- package/hedhog/data/dashboard_role.yaml +18 -12
- package/hedhog/data/menu.yaml +6 -0
- package/hedhog/data/route.yaml +57 -1
- package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +2 -1
- package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +24 -24
- package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +4 -4
- package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +23 -19
- package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +15 -14
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -58
- package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +3 -3
- package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +34 -33
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -14
- package/hedhog/frontend/app/users/page.tsx.ejs +322 -1
- package/hedhog/frontend/messages/en.json +19 -1
- package/hedhog/frontend/messages/pt.json +19 -1
- package/package.json +4 -4
- package/src/auth/auth.controller.ts +21 -20
- package/src/auth/auth.service.ts +63 -15
- package/src/profile/profile.service.ts +1 -1
- package/src/role/guards/role.guard.ts +36 -7
- package/src/session/session.service.ts +2 -2
- package/src/user/dto/reset-password.dto.ts +11 -0
- package/src/user/user.controller.ts +24 -14
- 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-
|
|
40
|
-
<MousePointerClick className="h-
|
|
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-
|
|
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-
|
|
40
|
-
<CalendarDays className="h-
|
|
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-
|
|
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-
|
|
40
|
-
<Clock className="h-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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} · {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} · {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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|