betterstart-cli 0.0.21 → 0.0.23
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/assets/adapters/next/templates/init/components/layouts/admin-sidebar-branding-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-version-item.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-current-row.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +3 -3
- package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +4 -4
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-drawer.tsx +3 -3
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-gallery-dialog.tsx +62 -64
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-item.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/sort-order-dialog.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/sort-order-drag-overlay-item.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/sort-order-item.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/data/navigation.ts +1 -3
- package/dist/assets/adapters/next/templates/init/lib/actions/auth/auth.ts +8 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/change-own-password.ts +59 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/index.ts +6 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/types.ts +10 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/users/change-user-password.ts +77 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/users/index.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/users/types.ts +10 -0
- package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +4 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +3 -18
- package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/media/media-page-content.tsx +13 -18
- package/dist/assets/adapters/next/templates/init/pages/profile/profile-form.tsx +133 -1
- package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/users/change-password-dialog.tsx +229 -0
- package/dist/assets/adapters/next/templates/init/pages/users/columns.tsx +22 -14
- package/dist/assets/adapters/next/templates/init/pages/users/create-user-dialog.tsx +99 -93
- package/dist/assets/adapters/next/templates/init/pages/users/delete-user-dialog.tsx +17 -25
- package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +5 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/index.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-bubble-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/remove-table-part-icon.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-button/color-highlight-button.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-content.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/highlight-color-button.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/source-color-highlight-popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/media-field-empty-state.tsx +19 -12
- package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/alert-dialog.tsx +15 -4
- package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/avatar.tsx +5 -5
- package/dist/assets/shared-assets/react-admin/ui/badge.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/button-group.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/button.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/card.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/carousel.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/command.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/dialog.tsx +29 -6
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/empty.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/hover-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +5 -6
- package/dist/assets/shared-assets/react-admin/ui/input-otp.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/input.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/item.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +8 -8
- package/dist/assets/shared-assets/react-admin/ui/native-select.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/progress.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/radio-group.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/scroll-area.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/select.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +10 -10
- package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/slider.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/switch.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/toggle-group.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/toggle.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
- package/dist/cli.js +5422 -4263
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@ import { useAuditLogs, useCleanupAuditLogs } from '@admin/hooks/use-audit-log'
|
|
|
21
21
|
import { useTableUtils } from '@admin/hooks/use-table-utils'
|
|
22
22
|
import { cn } from '@admin/utils/shared/cn'
|
|
23
23
|
import { type ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
|
24
|
-
import { GripVertical
|
|
24
|
+
import { GripVertical } from 'lucide-react'
|
|
25
25
|
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
|
|
26
26
|
import * as React from 'react'
|
|
27
27
|
import { toast } from 'sonner'
|
|
@@ -341,7 +341,7 @@ export function AuditLogPageContent() {
|
|
|
341
341
|
})
|
|
342
342
|
}, [setFilters])
|
|
343
343
|
|
|
344
|
-
const
|
|
344
|
+
const _runCleanup = React.useCallback(() => {
|
|
345
345
|
cleanupMutation.mutate(undefined, {
|
|
346
346
|
onSuccess: (result) => {
|
|
347
347
|
if (!result.success) {
|
|
@@ -386,22 +386,7 @@ export function AuditLogPageContent() {
|
|
|
386
386
|
|
|
387
387
|
return (
|
|
388
388
|
<React.Fragment>
|
|
389
|
-
<PageHeader
|
|
390
|
-
title="Audit Log"
|
|
391
|
-
actions={
|
|
392
|
-
<div className="flex items-center gap-1">
|
|
393
|
-
<Button
|
|
394
|
-
type="button"
|
|
395
|
-
variant="outline"
|
|
396
|
-
disabled={cleanupMutation.isPending}
|
|
397
|
-
onClick={runCleanup}
|
|
398
|
-
>
|
|
399
|
-
{cleanupMutation.isPending ? <LoaderCircle className="animate-spin" /> : <Trash2 />}
|
|
400
|
-
Cleanup
|
|
401
|
-
</Button>
|
|
402
|
-
</div>
|
|
403
|
-
}
|
|
404
|
-
/>
|
|
389
|
+
<PageHeader title="Audit Log" />
|
|
405
390
|
|
|
406
391
|
<EntityFiltersBar
|
|
407
392
|
filters={filtersConfig}
|
|
@@ -63,7 +63,7 @@ export default function DashboardPage() {
|
|
|
63
63
|
<div className="flex flex-col w-full gap-1">
|
|
64
64
|
{envVariables.map((env) => (
|
|
65
65
|
<div
|
|
66
|
-
className="flex flex-col w-full bg-card border shadow-card border-border rounded-
|
|
66
|
+
className="flex flex-col w-full bg-card border shadow-card border-border rounded-sm pl-4 pr-2 py-2"
|
|
67
67
|
key={env.name}
|
|
68
68
|
>
|
|
69
69
|
<div className="flex items-center justify-between">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function ForgotPasswordPageSkeleton() {
|
|
2
2
|
return (
|
|
3
|
-
<div className="overflow-hidden rounded-
|
|
3
|
+
<div className="overflow-hidden rounded-sm border bg-card">
|
|
4
4
|
<div className="grid md:grid-cols-[5fr_7fr]">
|
|
5
5
|
<div className="hidden min-h-[460px] bg-muted md:block" />
|
|
6
6
|
<div className="flex min-h-[460px] flex-col gap-10 p-6 md:p-20">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function LoginPageSkeleton() {
|
|
2
2
|
return (
|
|
3
|
-
<div className="overflow-hidden rounded-
|
|
3
|
+
<div className="overflow-hidden rounded-sm border bg-card">
|
|
4
4
|
<div className="grid md:grid-cols-[5fr_7fr]">
|
|
5
5
|
<div className="hidden min-h-[540px] bg-muted md:block" />
|
|
6
6
|
<div className="flex min-h-[540px] flex-col gap-10 p-6 md:p-20">
|
|
@@ -11,8 +11,7 @@ import { MediaGrid } from '@admin/components/shared/media/media-grid'
|
|
|
11
11
|
import { MediaGridPagination } from '@admin/components/shared/media/media-grid-pagination'
|
|
12
12
|
import { MediaUrlImporter } from '@admin/components/shared/media/media-url-importer'
|
|
13
13
|
import { PageHeader } from '@admin/components/shared/page-header'
|
|
14
|
-
import {
|
|
15
|
-
import { ButtonGroup } from '@admin/components/ui/button-group'
|
|
14
|
+
import { Tabs, TabsList, TabsTrigger } from '@admin/components/ui/tabs'
|
|
16
15
|
import * as mediaHooks from '@admin/hooks/use-media'
|
|
17
16
|
import { useMediaFiltersBar } from '@admin/hooks/use-media-filters-bar'
|
|
18
17
|
import { useUpload } from '@admin/hooks/use-upload'
|
|
@@ -172,22 +171,18 @@ export function MediaPageContent() {
|
|
|
172
171
|
<PageHeader
|
|
173
172
|
title="Media Gallery"
|
|
174
173
|
actions={
|
|
175
|
-
<
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<Upload />
|
|
188
|
-
Upload
|
|
189
|
-
</Button>
|
|
190
|
-
</ButtonGroup>
|
|
174
|
+
<Tabs defaultValue="upload">
|
|
175
|
+
<TabsList>
|
|
176
|
+
<TabsTrigger value="url" onClick={() => setActiveTab('url')}>
|
|
177
|
+
<Link className="size-3" />
|
|
178
|
+
Import from URL
|
|
179
|
+
</TabsTrigger>
|
|
180
|
+
<TabsTrigger value="upload" onClick={() => setActiveTab('upload')}>
|
|
181
|
+
<Upload className="size-3" />
|
|
182
|
+
Upload
|
|
183
|
+
</TabsTrigger>
|
|
184
|
+
</TabsList>
|
|
185
|
+
</Tabs>
|
|
191
186
|
}
|
|
192
187
|
/>
|
|
193
188
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
changeOwnPassword,
|
|
5
|
+
invalidateUsersCache,
|
|
6
|
+
isEmailConfigured,
|
|
7
|
+
updateEmail
|
|
8
|
+
} from '@admin/actions/profile'
|
|
4
9
|
import { authClient } from '@admin/auth/client'
|
|
5
10
|
import { MediaField } from '@admin/components/custom/media-field'
|
|
6
11
|
import { Avatar, AvatarFallback, AvatarImage } from '@admin/components/ui/avatar'
|
|
@@ -16,6 +21,7 @@ import {
|
|
|
16
21
|
import {
|
|
17
22
|
Form,
|
|
18
23
|
FormControl,
|
|
24
|
+
FormDescription,
|
|
19
25
|
FormField,
|
|
20
26
|
FormItem,
|
|
21
27
|
FormLabel,
|
|
@@ -40,7 +46,19 @@ const profileSchema = z.object({
|
|
|
40
46
|
currentPasswordForEmail: z.string().optional()
|
|
41
47
|
})
|
|
42
48
|
|
|
49
|
+
const passwordSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
currentPassword: z.string().min(1, 'Current password is required'),
|
|
52
|
+
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
|
53
|
+
confirmPassword: z.string().min(1, 'Confirm the new password')
|
|
54
|
+
})
|
|
55
|
+
.refine((values) => values.newPassword === values.confirmPassword, {
|
|
56
|
+
message: 'Passwords do not match',
|
|
57
|
+
path: ['confirmPassword']
|
|
58
|
+
})
|
|
59
|
+
|
|
43
60
|
type ProfileValues = z.infer<typeof profileSchema>
|
|
61
|
+
type PasswordValues = z.infer<typeof passwordSchema>
|
|
44
62
|
|
|
45
63
|
interface ProfileFormProps {
|
|
46
64
|
user: {
|
|
@@ -53,6 +71,7 @@ interface ProfileFormProps {
|
|
|
53
71
|
export function ProfileForm({ user }: ProfileFormProps) {
|
|
54
72
|
const router = useRouter()
|
|
55
73
|
const [profilePending, startProfileTransition] = React.useTransition()
|
|
74
|
+
const [passwordPending, startPasswordTransition] = React.useTransition()
|
|
56
75
|
const [resetPending, startResetTransition] = React.useTransition()
|
|
57
76
|
|
|
58
77
|
const profileForm = useForm<ProfileValues>({
|
|
@@ -64,6 +83,16 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
64
83
|
currentPasswordForEmail: ''
|
|
65
84
|
}
|
|
66
85
|
})
|
|
86
|
+
const passwordForm = useForm<PasswordValues>({
|
|
87
|
+
resolver: standardSchemaResolver(passwordSchema),
|
|
88
|
+
defaultValues: {
|
|
89
|
+
currentPassword: '',
|
|
90
|
+
newPassword: '',
|
|
91
|
+
confirmPassword: ''
|
|
92
|
+
},
|
|
93
|
+
mode: 'onBlur',
|
|
94
|
+
reValidateMode: 'onChange'
|
|
95
|
+
})
|
|
67
96
|
|
|
68
97
|
const emailDirty = profileForm.watch('email') !== user.email
|
|
69
98
|
const imageValue = profileForm.watch('image')
|
|
@@ -115,6 +144,31 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
115
144
|
})
|
|
116
145
|
}
|
|
117
146
|
|
|
147
|
+
function onPasswordSubmit(values: PasswordValues) {
|
|
148
|
+
startPasswordTransition(async () => {
|
|
149
|
+
try {
|
|
150
|
+
const result = await changeOwnPassword({
|
|
151
|
+
currentPassword: values.currentPassword,
|
|
152
|
+
newPassword: values.newPassword
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if (!result.success) {
|
|
156
|
+
toast.error(result.error || 'Failed to change password')
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
toast.success('Password changed successfully')
|
|
161
|
+
passwordForm.reset({
|
|
162
|
+
currentPassword: '',
|
|
163
|
+
newPassword: '',
|
|
164
|
+
confirmPassword: ''
|
|
165
|
+
})
|
|
166
|
+
} catch {
|
|
167
|
+
toast.error('An error occurred while changing your password')
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
118
172
|
function handleResetPassword() {
|
|
119
173
|
startResetTransition(async () => {
|
|
120
174
|
try {
|
|
@@ -253,6 +307,84 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
253
307
|
</form>
|
|
254
308
|
</Form>
|
|
255
309
|
|
|
310
|
+
<Form {...passwordForm}>
|
|
311
|
+
<form onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}>
|
|
312
|
+
<Card>
|
|
313
|
+
<CardHeader>
|
|
314
|
+
<CardTitle>Change Password</CardTitle>
|
|
315
|
+
<CardDescription>Update the password for your account</CardDescription>
|
|
316
|
+
</CardHeader>
|
|
317
|
+
<CardContent className="space-y-5">
|
|
318
|
+
<FormField
|
|
319
|
+
control={passwordForm.control}
|
|
320
|
+
name="currentPassword"
|
|
321
|
+
render={({ field }) => (
|
|
322
|
+
<FormItem>
|
|
323
|
+
<FormLabel>Current Password</FormLabel>
|
|
324
|
+
<FormControl>
|
|
325
|
+
<Input
|
|
326
|
+
type="password"
|
|
327
|
+
autoComplete="current-password"
|
|
328
|
+
disabled={passwordPending}
|
|
329
|
+
{...field}
|
|
330
|
+
/>
|
|
331
|
+
</FormControl>
|
|
332
|
+
<FormMessage />
|
|
333
|
+
</FormItem>
|
|
334
|
+
)}
|
|
335
|
+
/>
|
|
336
|
+
<FormField
|
|
337
|
+
control={passwordForm.control}
|
|
338
|
+
name="newPassword"
|
|
339
|
+
render={({ field }) => (
|
|
340
|
+
<FormItem>
|
|
341
|
+
<FormLabel>New Password</FormLabel>
|
|
342
|
+
<FormControl>
|
|
343
|
+
<Input
|
|
344
|
+
type="password"
|
|
345
|
+
autoComplete="new-password"
|
|
346
|
+
disabled={passwordPending}
|
|
347
|
+
{...field}
|
|
348
|
+
/>
|
|
349
|
+
</FormControl>
|
|
350
|
+
<FormDescription>Use at least 8 characters.</FormDescription>
|
|
351
|
+
<FormMessage />
|
|
352
|
+
</FormItem>
|
|
353
|
+
)}
|
|
354
|
+
/>
|
|
355
|
+
<FormField
|
|
356
|
+
control={passwordForm.control}
|
|
357
|
+
name="confirmPassword"
|
|
358
|
+
render={({ field }) => (
|
|
359
|
+
<FormItem>
|
|
360
|
+
<FormLabel>Confirm Password</FormLabel>
|
|
361
|
+
<FormControl>
|
|
362
|
+
<Input
|
|
363
|
+
type="password"
|
|
364
|
+
autoComplete="new-password"
|
|
365
|
+
disabled={passwordPending}
|
|
366
|
+
{...field}
|
|
367
|
+
/>
|
|
368
|
+
</FormControl>
|
|
369
|
+
<FormMessage />
|
|
370
|
+
</FormItem>
|
|
371
|
+
)}
|
|
372
|
+
/>
|
|
373
|
+
</CardContent>
|
|
374
|
+
<CardFooter>
|
|
375
|
+
<Button
|
|
376
|
+
type="submit"
|
|
377
|
+
disabled={passwordPending || !passwordForm.formState.isDirty}
|
|
378
|
+
className="ml-auto min-w-25"
|
|
379
|
+
>
|
|
380
|
+
{passwordPending && <LoaderCircle className="animate-spin" />}
|
|
381
|
+
{passwordPending ? 'Changing...' : 'Change Password'}
|
|
382
|
+
</Button>
|
|
383
|
+
</CardFooter>
|
|
384
|
+
</Card>
|
|
385
|
+
</form>
|
|
386
|
+
</Form>
|
|
387
|
+
|
|
256
388
|
<Card>
|
|
257
389
|
<CardHeader>
|
|
258
390
|
<CardTitle>Reset Password</CardTitle>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function ResetPasswordPageSkeleton() {
|
|
2
2
|
return (
|
|
3
|
-
<div className="overflow-hidden rounded-
|
|
3
|
+
<div className="overflow-hidden rounded-sm border bg-card">
|
|
4
4
|
<div className="grid md:grid-cols-[5fr_7fr]">
|
|
5
5
|
<div className="hidden min-h-[520px] bg-muted md:block" />
|
|
6
6
|
<div className="flex min-h-[520px] flex-col gap-10 p-6 md:p-20">
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { changeOwnPassword } from '@admin/actions/profile'
|
|
4
|
+
import { changeUserPassword } from '@admin/actions/users'
|
|
5
|
+
import { Button } from '@admin/components/ui/button'
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogBody,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
DialogTrigger
|
|
15
|
+
} from '@admin/components/ui/dialog'
|
|
16
|
+
import { DropdownMenuItem } from '@admin/components/ui/dropdown-menu'
|
|
17
|
+
import {
|
|
18
|
+
Form,
|
|
19
|
+
FormControl,
|
|
20
|
+
FormDescription,
|
|
21
|
+
FormField,
|
|
22
|
+
FormItem,
|
|
23
|
+
FormLabel,
|
|
24
|
+
FormMessage
|
|
25
|
+
} from '@admin/components/ui/form'
|
|
26
|
+
import { Input } from '@admin/components/ui/input'
|
|
27
|
+
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
|
|
28
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
29
|
+
import { KeySquare, LoaderCircle } from 'lucide-react'
|
|
30
|
+
import * as React from 'react'
|
|
31
|
+
import { useForm } from 'react-hook-form'
|
|
32
|
+
import { toast } from 'sonner'
|
|
33
|
+
import { z } from 'zod/v3'
|
|
34
|
+
|
|
35
|
+
const passwordFieldsSchema = z.object({
|
|
36
|
+
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
|
37
|
+
confirmPassword: z.string().min(1, 'Confirm the new password')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const ownPasswordSchema = passwordFieldsSchema
|
|
41
|
+
.extend({
|
|
42
|
+
currentPassword: z.string().min(1, 'Current password is required')
|
|
43
|
+
})
|
|
44
|
+
.refine((values) => values.newPassword === values.confirmPassword, {
|
|
45
|
+
message: 'Passwords do not match',
|
|
46
|
+
path: ['confirmPassword']
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const userPasswordSchema = passwordFieldsSchema
|
|
50
|
+
.extend({
|
|
51
|
+
currentPassword: z.string().optional()
|
|
52
|
+
})
|
|
53
|
+
.refine((values) => values.newPassword === values.confirmPassword, {
|
|
54
|
+
message: 'Passwords do not match',
|
|
55
|
+
path: ['confirmPassword']
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
type ChangePasswordFormValues = z.infer<typeof userPasswordSchema>
|
|
59
|
+
|
|
60
|
+
const defaultValues: ChangePasswordFormValues = {
|
|
61
|
+
currentPassword: '',
|
|
62
|
+
newPassword: '',
|
|
63
|
+
confirmPassword: ''
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ChangePasswordDialogProps {
|
|
67
|
+
userId: string
|
|
68
|
+
userName: string
|
|
69
|
+
userEmail: string
|
|
70
|
+
isCurrentUser: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function ChangePasswordDialog({
|
|
74
|
+
userId,
|
|
75
|
+
userName,
|
|
76
|
+
userEmail,
|
|
77
|
+
isCurrentUser
|
|
78
|
+
}: ChangePasswordDialogProps) {
|
|
79
|
+
const [open, setOpen] = React.useState(false)
|
|
80
|
+
const queryClient = useQueryClient()
|
|
81
|
+
const form = useForm<ChangePasswordFormValues>({
|
|
82
|
+
resolver: standardSchemaResolver(isCurrentUser ? ownPasswordSchema : userPasswordSchema),
|
|
83
|
+
defaultValues,
|
|
84
|
+
mode: 'onBlur',
|
|
85
|
+
reValidateMode: 'onChange'
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const mutation = useMutation({
|
|
89
|
+
mutationFn: async (values: ChangePasswordFormValues) => {
|
|
90
|
+
const result = isCurrentUser
|
|
91
|
+
? await changeOwnPassword({
|
|
92
|
+
currentPassword: values.currentPassword ?? '',
|
|
93
|
+
newPassword: values.newPassword
|
|
94
|
+
})
|
|
95
|
+
: await changeUserPassword({
|
|
96
|
+
userId,
|
|
97
|
+
newPassword: values.newPassword
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
throw new Error(result.error || 'Failed to change password')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
},
|
|
106
|
+
onSuccess: async () => {
|
|
107
|
+
toast.success(
|
|
108
|
+
isCurrentUser ? 'Password changed successfully' : `Password changed for ${userName}`
|
|
109
|
+
)
|
|
110
|
+
await queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
111
|
+
resetFormState()
|
|
112
|
+
setOpen(false)
|
|
113
|
+
},
|
|
114
|
+
onError: (error: Error) => {
|
|
115
|
+
toast.error(error.message || 'An unexpected error occurred')
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const isPending = mutation.isPending
|
|
120
|
+
|
|
121
|
+
function resetFormState() {
|
|
122
|
+
form.reset(defaultValues)
|
|
123
|
+
mutation.reset()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleOpenChange(nextOpen: boolean) {
|
|
127
|
+
setOpen(nextOpen)
|
|
128
|
+
|
|
129
|
+
if (!nextOpen) {
|
|
130
|
+
resetFormState()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
136
|
+
<DialogTrigger asChild>
|
|
137
|
+
<DropdownMenuItem onSelect={(event) => event.preventDefault()}>
|
|
138
|
+
<KeySquare className="size-4" />
|
|
139
|
+
Change password
|
|
140
|
+
</DropdownMenuItem>
|
|
141
|
+
</DialogTrigger>
|
|
142
|
+
<DialogContent className="sm:max-w-135">
|
|
143
|
+
<Form {...form}>
|
|
144
|
+
<form onSubmit={form.handleSubmit((values) => mutation.mutate(values))}>
|
|
145
|
+
<DialogHeader>
|
|
146
|
+
<DialogTitle>Change Password</DialogTitle>
|
|
147
|
+
<DialogDescription>
|
|
148
|
+
{isCurrentUser
|
|
149
|
+
? 'Set a new password for your account.'
|
|
150
|
+
: `Set a new password for ${userName || userEmail}.`}
|
|
151
|
+
</DialogDescription>
|
|
152
|
+
</DialogHeader>
|
|
153
|
+
|
|
154
|
+
<DialogBody>
|
|
155
|
+
<div className="space-y-4">
|
|
156
|
+
{isCurrentUser ? (
|
|
157
|
+
<FormField
|
|
158
|
+
control={form.control}
|
|
159
|
+
name="currentPassword"
|
|
160
|
+
render={({ field }) => (
|
|
161
|
+
<FormItem>
|
|
162
|
+
<FormLabel>Current Password</FormLabel>
|
|
163
|
+
<FormControl>
|
|
164
|
+
<Input
|
|
165
|
+
type="password"
|
|
166
|
+
autoComplete="current-password"
|
|
167
|
+
disabled={isPending}
|
|
168
|
+
autoFocus
|
|
169
|
+
{...field}
|
|
170
|
+
/>
|
|
171
|
+
</FormControl>
|
|
172
|
+
<FormMessage />
|
|
173
|
+
</FormItem>
|
|
174
|
+
)}
|
|
175
|
+
/>
|
|
176
|
+
) : null}
|
|
177
|
+
<FormField
|
|
178
|
+
control={form.control}
|
|
179
|
+
name="newPassword"
|
|
180
|
+
render={({ field }) => (
|
|
181
|
+
<FormItem>
|
|
182
|
+
<FormLabel>New Password</FormLabel>
|
|
183
|
+
<FormControl>
|
|
184
|
+
<Input
|
|
185
|
+
type="password"
|
|
186
|
+
autoComplete="new-password"
|
|
187
|
+
disabled={isPending}
|
|
188
|
+
autoFocus={!isCurrentUser}
|
|
189
|
+
{...field}
|
|
190
|
+
/>
|
|
191
|
+
</FormControl>
|
|
192
|
+
<FormDescription>Use at least 8 characters.</FormDescription>
|
|
193
|
+
<FormMessage />
|
|
194
|
+
</FormItem>
|
|
195
|
+
)}
|
|
196
|
+
/>
|
|
197
|
+
<FormField
|
|
198
|
+
control={form.control}
|
|
199
|
+
name="confirmPassword"
|
|
200
|
+
render={({ field }) => (
|
|
201
|
+
<FormItem>
|
|
202
|
+
<FormLabel>Confirm Password</FormLabel>
|
|
203
|
+
<FormControl>
|
|
204
|
+
<Input
|
|
205
|
+
type="password"
|
|
206
|
+
autoComplete="new-password"
|
|
207
|
+
disabled={isPending}
|
|
208
|
+
{...field}
|
|
209
|
+
/>
|
|
210
|
+
</FormControl>
|
|
211
|
+
<FormMessage />
|
|
212
|
+
</FormItem>
|
|
213
|
+
)}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
</DialogBody>
|
|
217
|
+
|
|
218
|
+
<DialogFooter showCloseButton={true}>
|
|
219
|
+
<Button type="submit" disabled={isPending} size="lg">
|
|
220
|
+
{isPending ? <LoaderCircle className="animate-spin" /> : null}
|
|
221
|
+
{isPending ? 'Changing...' : 'Change Password'}
|
|
222
|
+
</Button>
|
|
223
|
+
</DialogFooter>
|
|
224
|
+
</form>
|
|
225
|
+
</Form>
|
|
226
|
+
</DialogContent>
|
|
227
|
+
</Dialog>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -5,13 +5,13 @@ import { Button } from '@admin/components/ui/button'
|
|
|
5
5
|
import {
|
|
6
6
|
DropdownMenu,
|
|
7
7
|
DropdownMenuContent,
|
|
8
|
-
DropdownMenuItem,
|
|
9
8
|
DropdownMenuTrigger
|
|
10
9
|
} from '@admin/components/ui/dropdown-menu'
|
|
11
|
-
import type
|
|
10
|
+
import { type User, UserRole } from '@admin/types/auth'
|
|
12
11
|
import { getUserInitials } from '@admin/utils/user/user'
|
|
13
12
|
import type { ColumnDef } from '@tanstack/react-table'
|
|
14
|
-
import { Edit,
|
|
13
|
+
import { Edit, MoreHorizontal, ShieldCheck } from 'lucide-react'
|
|
14
|
+
import { ChangePasswordDialog } from './change-password-dialog'
|
|
15
15
|
import { DeleteUserDialog } from './delete-user-dialog'
|
|
16
16
|
import { EditRoleDialog } from './edit-role-dialog'
|
|
17
17
|
|
|
@@ -138,9 +138,11 @@ export const columns: ColumnDef<User>[] = [
|
|
|
138
138
|
meta: { width: '80px' },
|
|
139
139
|
cell: ({ row, table }) => {
|
|
140
140
|
const currentUser = table.options.meta?.currentUser
|
|
141
|
-
const isCurrentUser =
|
|
141
|
+
const isCurrentUser =
|
|
142
|
+
currentUser?.id === row.original.id || currentUser?.email === row.original.email
|
|
143
|
+
const canChangePassword = isCurrentUser || currentUser?.role === UserRole.ADMIN
|
|
142
144
|
|
|
143
|
-
if (isCurrentUser) return null
|
|
145
|
+
if (!isCurrentUser && !currentUser) return null
|
|
144
146
|
|
|
145
147
|
return (
|
|
146
148
|
<div className="flex justify-end">
|
|
@@ -152,15 +154,21 @@ export const columns: ColumnDef<User>[] = [
|
|
|
152
154
|
</Button>
|
|
153
155
|
</DropdownMenuTrigger>
|
|
154
156
|
<DropdownMenuContent align="end">
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
{!isCurrentUser ? (
|
|
158
|
+
<DeleteUserDialog
|
|
159
|
+
userId={row.original.id}
|
|
160
|
+
userName={row.original.name}
|
|
161
|
+
isCurrentUser={isCurrentUser}
|
|
162
|
+
/>
|
|
163
|
+
) : null}
|
|
164
|
+
{canChangePassword ? (
|
|
165
|
+
<ChangePasswordDialog
|
|
166
|
+
userId={row.original.id}
|
|
167
|
+
userName={row.original.name}
|
|
168
|
+
userEmail={row.original.email}
|
|
169
|
+
isCurrentUser={isCurrentUser}
|
|
170
|
+
/>
|
|
171
|
+
) : null}
|
|
164
172
|
</DropdownMenuContent>
|
|
165
173
|
</DropdownMenu>
|
|
166
174
|
</div>
|