@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.
- package/README.md +60 -0
- 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/dashboard/dashboard-core/dashboard-core.controller.d.ts +12 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +12 -0
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js +25 -0
- package/dist/dashboard/dashboard-core/dashboard-core.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 +74 -12
- package/hedhog/data/dashboard_component_role.yaml +223 -145
- package/hedhog/data/dashboard_item.yaml +42 -22
- package/hedhog/data/dashboard_role.yaml +18 -12
- package/hedhog/data/menu.yaml +6 -0
- package/hedhog/data/route.yaml +65 -1
- package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +2 -1
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +17 -17
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +23 -12
- package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +80 -5
- package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +17 -13
- package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +16 -12
- package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +27 -16
- package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +13 -9
- package/hedhog/frontend/app/dashboard/components/widgets/menus-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -58
- package/hedhog/frontend/app/dashboard/components/widgets/routes-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +6 -6
- package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +6 -6
- package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +6 -6
- package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +6 -6
- package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +15 -11
- package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +18 -15
- package/hedhog/frontend/app/dashboard/dashboard.css.ejs +20 -4
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -14
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +5 -11
- package/hedhog/frontend/app/users/page.tsx.ejs +331 -10
- package/hedhog/frontend/messages/en.json +29 -3
- package/hedhog/frontend/messages/pt.json +29 -3
- package/package.json +4 -4
- package/src/auth/auth.controller.ts +21 -20
- package/src/auth/auth.service.ts +63 -15
- package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
- package/src/dashboard/dashboard-core/dashboard-core.service.ts +34 -0
- 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
|
@@ -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-
|
|
92
|
-
<div className="flex items-center gap-2">
|
|
93
|
-
<Globe className="h-
|
|
94
|
-
<div>
|
|
95
|
-
<CardTitle className="text-
|
|
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>
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
172
|
+
<span className="wrap-break-word text-[11px] text-muted-foreground sm:text-xs">
|
|
170
173
|
{browser} · {ip}
|
|
171
174
|
</span>
|
|
172
|
-
<div className="flex items-center gap-
|
|
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
|
|
38
|
-
.
|
|
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
|
|
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
|
|
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 {
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
<
|
|
572
|
-
<
|
|
573
|
-
<
|
|
574
|
-
<
|
|
575
|
-
<
|
|
576
|
-
</
|
|
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="
|
|
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
|
-
</
|
|
665
|
-
</
|
|
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
|