betterstart-cli 0.0.107 → 0.0.109
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/admin-globals.css +2 -62
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-header.tsx +2 -19
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-providers.tsx +4 -7
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-user-menu.tsx +0 -1
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table-pagination.tsx +3 -1
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/copyable-code-block.tsx +2 -5
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/dev-mode-code-mirror.tsx +4 -5
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/dev-mode-types.ts +0 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +1 -0
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-pagination.tsx +3 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +89 -78
- package/dist/assets/adapters/next/templates/init/components/shared/search-input.tsx +5 -1
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-source-mode.tsx +2 -11
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor.ts +1 -2
- package/dist/assets/adapters/next/templates/init/pages/forgot-password-form.tsx +42 -37
- package/dist/assets/adapters/next/templates/init/pages/login-form.tsx +95 -71
- package/dist/assets/adapters/next/templates/init/pages/profile/profile-form.tsx +7 -7
- package/dist/assets/adapters/next/templates/init/pages/reset-password-form.tsx +88 -69
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/form-notifications-drawer.tsx +1 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-endpoint-dialog.tsx +26 -8
- package/dist/assets/adapters/next/templates/init/pages/users/change-password-dialog.tsx +8 -10
- package/dist/assets/adapters/next/templates/init/pages/users/create-user-dialog.tsx +8 -5
- package/dist/assets/adapters/next/templates/init/pages/users/delete-user-dialog.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/users/edit-role-dialog.tsx +76 -33
- package/dist/assets/shared-assets/react-admin/custom/content-editor/main-toolbar-content.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/custom/content-editor/mobile-toolbar-content.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +1 -5
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +3 -15
- package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +0 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/blockquote-button/blockquote-button.tsx +2 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/code-block-button/code-block-button.tsx +2 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-button/color-highlight-button.tsx +2 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +2 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/mark-button/mark-button.tsx +2 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/undo-redo-button/undo-redo-button.tsx +2 -1
- package/dist/assets/shared-assets/react-admin/ui/avatar.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/badge.tsx +5 -3
- package/dist/assets/shared-assets/react-admin/ui/button.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/calendar.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/chart.tsx +5 -16
- package/dist/assets/shared-assets/react-admin/ui/checkbox.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +12 -2
- package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +24 -21
- package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +9 -3
- 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/kbd.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/native-select.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/radio-group.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/select.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/sonner.tsx +2 -5
- 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.tsx +1 -1
- package/dist/{chunk-OYU73Q2W.js → chunk-PSDUGFMD.js} +2 -2
- package/dist/{chunk-OYU73Q2W.js.map → chunk-PSDUGFMD.js.map} +1 -1
- package/dist/cli.js +240 -193
- package/dist/cli.js.map +1 -1
- package/dist/{read-namespaced-template-EIH2NPDJ.js → read-namespaced-template-TSATYQPP.js} +2 -2
- package/package.json +1 -1
- package/dist/assets/adapters/next/templates/init/hooks/use-admin-theme.tsx +0 -72
- package/dist/assets/adapters/next/templates/init/utils/theme/system.ts +0 -6
- /package/dist/{read-namespaced-template-EIH2NPDJ.js.map → read-namespaced-template-TSATYQPP.js.map} +0 -0
|
@@ -1,49 +1,68 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import type { FieldErrors } from 'react-hook-form'
|
|
4
5
|
import { isEmailConfigured } from '@admin/actions/profile'
|
|
5
6
|
import { authClient } from '@admin/auth/client'
|
|
6
7
|
import { Button } from '@admin/components/ui/button'
|
|
7
8
|
import { Card, CardContent } from '@admin/components/ui/card'
|
|
8
9
|
import { Field, FieldGroup, FieldLabel } from '@admin/components/ui/field'
|
|
10
|
+
import { Form } from '@admin/components/ui/form'
|
|
9
11
|
import { Input } from '@admin/components/ui/input'
|
|
10
12
|
import { Spinner } from '@admin/components/ui/spinner'
|
|
11
13
|
import { cn } from '@admin/utils/shared/cn'
|
|
14
|
+
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
|
|
12
15
|
import Image from 'next/image'
|
|
13
16
|
import Link from 'next/link'
|
|
17
|
+
import { useForm } from 'react-hook-form'
|
|
18
|
+
import { toast } from 'sonner'
|
|
19
|
+
import { z } from 'zod/v3'
|
|
20
|
+
|
|
21
|
+
const forgotPasswordSchema = z.object({
|
|
22
|
+
email: z.string().trim().min(1, 'Email is required').email('Enter a valid email address')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
type ForgotPasswordFormValues = z.infer<typeof forgotPasswordSchema>
|
|
14
26
|
|
|
15
27
|
export function ForgotPasswordForm({ className, ...props }: React.ComponentProps<'div'>) {
|
|
16
|
-
const [email, setEmail] = React.useState('')
|
|
17
|
-
const [error, setError] = React.useState<string | null>(null)
|
|
18
|
-
const [success, setSuccess] = React.useState(false)
|
|
19
28
|
const [isPending, startTransition] = React.useTransition()
|
|
29
|
+
const form = useForm<ForgotPasswordFormValues>({
|
|
30
|
+
resolver: standardSchemaResolver(forgotPasswordSchema),
|
|
31
|
+
defaultValues: { email: '' },
|
|
32
|
+
mode: 'onChange',
|
|
33
|
+
reValidateMode: 'onChange'
|
|
34
|
+
})
|
|
20
35
|
|
|
21
|
-
const handleSubmit = (
|
|
22
|
-
e.preventDefault()
|
|
23
|
-
setError(null)
|
|
24
|
-
|
|
36
|
+
const handleSubmit = (values: ForgotPasswordFormValues) => {
|
|
25
37
|
startTransition(async () => {
|
|
26
38
|
try {
|
|
27
39
|
const emailReady = await isEmailConfigured()
|
|
28
40
|
if (!emailReady) {
|
|
29
|
-
|
|
41
|
+
toast.error('Email delivery is not configured for this project.')
|
|
30
42
|
return
|
|
31
43
|
}
|
|
32
44
|
const { error } = await authClient.requestPasswordReset({
|
|
33
|
-
email,
|
|
45
|
+
email: values.email,
|
|
34
46
|
redirectTo: '/admin/reset-password'
|
|
35
47
|
})
|
|
36
48
|
if (error) {
|
|
37
|
-
|
|
49
|
+
toast.error(error.message || 'Failed to send reset link')
|
|
38
50
|
return
|
|
39
51
|
}
|
|
40
|
-
|
|
52
|
+
toast.success(
|
|
53
|
+
'If an account exists, we sent a password reset link. Check your inbox and spam folder.'
|
|
54
|
+
)
|
|
41
55
|
} catch {
|
|
42
|
-
|
|
56
|
+
toast.error('Failed to send the reset link. Try again.')
|
|
43
57
|
}
|
|
44
58
|
})
|
|
45
59
|
}
|
|
46
60
|
|
|
61
|
+
const handleInvalid = (errors: FieldErrors<ForgotPasswordFormValues>) => {
|
|
62
|
+
const message = errors.email?.message
|
|
63
|
+
toast.error(typeof message === 'string' ? message : 'Enter a valid email address.')
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
return (
|
|
48
67
|
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
|
49
68
|
<Card className="overflow-hidden p-0">
|
|
@@ -54,25 +73,16 @@ export function ForgotPasswordForm({ className, ...props }: React.ComponentProps
|
|
|
54
73
|
alt=""
|
|
55
74
|
fill
|
|
56
75
|
sizes="(min-width: 768px) 42vw, 0px"
|
|
57
|
-
className="object-cover
|
|
76
|
+
className="object-cover"
|
|
58
77
|
/>
|
|
59
78
|
</div>
|
|
60
79
|
<div className="p-6 md:p-20">
|
|
61
|
-
{
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
inbox and spam folder.
|
|
68
|
-
</p>
|
|
69
|
-
</div>
|
|
70
|
-
<Link href="/admin/login" className="text-sm underline-offset-4 hover:underline">
|
|
71
|
-
Back to sign in
|
|
72
|
-
</Link>
|
|
73
|
-
</FieldGroup>
|
|
74
|
-
) : (
|
|
75
|
-
<form onSubmit={handleSubmit}>
|
|
80
|
+
<Form {...form}>
|
|
81
|
+
<form
|
|
82
|
+
autoComplete="off"
|
|
83
|
+
noValidate
|
|
84
|
+
onSubmit={form.handleSubmit(handleSubmit, handleInvalid)}
|
|
85
|
+
>
|
|
76
86
|
<FieldGroup className="gap-10 pb-12">
|
|
77
87
|
<div className="flex flex-col items-start">
|
|
78
88
|
<h1 className="text-xl font-medium">Forgot password</h1>
|
|
@@ -81,21 +91,16 @@ export function ForgotPasswordForm({ className, ...props }: React.ComponentProps
|
|
|
81
91
|
enabled
|
|
82
92
|
</p>
|
|
83
93
|
</div>
|
|
84
|
-
{error ? (
|
|
85
|
-
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
|
86
|
-
{error}
|
|
87
|
-
</div>
|
|
88
|
-
) : null}
|
|
89
94
|
<div className="flex flex-col gap-4">
|
|
90
95
|
<Field>
|
|
91
96
|
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
92
97
|
<Input
|
|
93
98
|
id="email"
|
|
94
99
|
type="email"
|
|
95
|
-
autoComplete="
|
|
100
|
+
autoComplete="off"
|
|
96
101
|
placeholder="m@example.com"
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
103
|
+
{...form.register('email')}
|
|
99
104
|
required
|
|
100
105
|
disabled={isPending}
|
|
101
106
|
/>
|
|
@@ -115,7 +120,7 @@ export function ForgotPasswordForm({ className, ...props }: React.ComponentProps
|
|
|
115
120
|
</div>
|
|
116
121
|
</FieldGroup>
|
|
117
122
|
</form>
|
|
118
|
-
|
|
123
|
+
</Form>
|
|
119
124
|
</div>
|
|
120
125
|
</CardContent>
|
|
121
126
|
</Card>
|
|
@@ -1,41 +1,64 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import type { FieldErrors } from 'react-hook-form'
|
|
4
5
|
import { authClient } from '@admin/auth/client'
|
|
5
6
|
import { Button } from '@admin/components/ui/button'
|
|
6
7
|
import { Card, CardContent } from '@admin/components/ui/card'
|
|
7
8
|
import { Field, FieldGroup, FieldLabel } from '@admin/components/ui/field'
|
|
9
|
+
import { Form } from '@admin/components/ui/form'
|
|
8
10
|
import { Input } from '@admin/components/ui/input'
|
|
9
11
|
import { Spinner } from '@admin/components/ui/spinner'
|
|
10
12
|
import { getAdminPostLoginPath } from '@admin/utils/auth/get-admin-post-login-path'
|
|
11
13
|
import { cn } from '@admin/utils/shared/cn'
|
|
14
|
+
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
|
|
12
15
|
import Image from 'next/image'
|
|
13
16
|
import Link from 'next/link'
|
|
14
17
|
import { useRouter } from 'next/navigation'
|
|
15
18
|
import { parseAsStringLiteral, useQueryState } from 'nuqs'
|
|
19
|
+
import { useForm } from 'react-hook-form'
|
|
20
|
+
import { toast } from 'sonner'
|
|
21
|
+
import { z } from 'zod/v3'
|
|
22
|
+
|
|
23
|
+
const loginSchema = z.object({
|
|
24
|
+
email: z.string().trim().min(1, 'Email is required').email('Enter a valid email address'),
|
|
25
|
+
password: z.string().min(1, 'Password is required')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
type LoginFormValues = z.infer<typeof loginSchema>
|
|
16
29
|
|
|
17
30
|
export function LoginForm({ className, ...props }: React.ComponentProps<'div'>) {
|
|
18
31
|
const router = useRouter()
|
|
19
32
|
const [reset] = useQueryState('reset', parseAsStringLiteral(['success']))
|
|
20
|
-
const resetSuccess = reset === 'success'
|
|
21
|
-
const [email, setEmail] = React.useState('')
|
|
22
|
-
const [password, setPassword] = React.useState('')
|
|
23
|
-
const [error, setError] = React.useState<string | null>(null)
|
|
24
33
|
const [isPending, startTransition] = React.useTransition()
|
|
34
|
+
const form = useForm<LoginFormValues>({
|
|
35
|
+
resolver: standardSchemaResolver(loginSchema),
|
|
36
|
+
defaultValues: {
|
|
37
|
+
email: '',
|
|
38
|
+
password: ''
|
|
39
|
+
},
|
|
40
|
+
mode: 'onChange',
|
|
41
|
+
reValidateMode: 'onChange'
|
|
42
|
+
})
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (reset === 'success') {
|
|
46
|
+
toast.success('Password reset. Sign in with your new password.', {
|
|
47
|
+
id: 'admin-login-password-reset-success'
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}, [reset])
|
|
29
51
|
|
|
52
|
+
const handleSubmit = (values: LoginFormValues) => {
|
|
30
53
|
startTransition(async () => {
|
|
31
54
|
try {
|
|
32
55
|
const result = await authClient.signIn.email({
|
|
33
|
-
email,
|
|
34
|
-
password
|
|
56
|
+
email: values.email,
|
|
57
|
+
password: values.password
|
|
35
58
|
})
|
|
36
59
|
|
|
37
60
|
if (result.error) {
|
|
38
|
-
|
|
61
|
+
toast.error(result.error.message || 'Invalid email or password')
|
|
39
62
|
return
|
|
40
63
|
}
|
|
41
64
|
|
|
@@ -45,11 +68,16 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
|
|
|
45
68
|
router.replace(getAdminPostLoginPath(role))
|
|
46
69
|
router.refresh()
|
|
47
70
|
} catch {
|
|
48
|
-
|
|
71
|
+
toast.error('Failed to sign you in. Try again.')
|
|
49
72
|
}
|
|
50
73
|
})
|
|
51
74
|
}
|
|
52
75
|
|
|
76
|
+
const handleInvalid = (errors: FieldErrors<LoginFormValues>) => {
|
|
77
|
+
const message = errors.email?.message ?? errors.password?.message
|
|
78
|
+
toast.error(typeof message === 'string' ? message : 'Please check your sign-in details.')
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
return (
|
|
54
82
|
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
|
55
83
|
<Card className="overflow-hidden p-0">
|
|
@@ -60,70 +88,66 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
|
|
|
60
88
|
alt=""
|
|
61
89
|
fill
|
|
62
90
|
sizes="(min-width: 768px) 42vw, 0px"
|
|
63
|
-
className="object-cover
|
|
91
|
+
className="object-cover"
|
|
64
92
|
/>
|
|
65
93
|
</div>
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
<Form {...form}>
|
|
95
|
+
<form
|
|
96
|
+
autoComplete="off"
|
|
97
|
+
noValidate
|
|
98
|
+
onSubmit={form.handleSubmit(handleSubmit, handleInvalid)}
|
|
99
|
+
className="p-6 md:p-20"
|
|
100
|
+
>
|
|
101
|
+
<FieldGroup className="gap-10 pb-12">
|
|
102
|
+
<div className="flex flex-col items-start">
|
|
103
|
+
<h1 className="text-xl font-medium">Sign In</h1>
|
|
75
104
|
</div>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
<div className="flex flex-col gap-4">
|
|
106
|
+
<Field>
|
|
107
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
108
|
+
<Input
|
|
109
|
+
id="email"
|
|
110
|
+
type="email"
|
|
111
|
+
autoComplete="off"
|
|
112
|
+
placeholder="m@example.com"
|
|
113
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
114
|
+
{...form.register('email')}
|
|
115
|
+
required
|
|
116
|
+
disabled={isPending}
|
|
117
|
+
/>
|
|
118
|
+
</Field>
|
|
119
|
+
<Field>
|
|
120
|
+
<div className="flex items-center">
|
|
121
|
+
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
122
|
+
<Link
|
|
123
|
+
href="/admin/forgot-password"
|
|
124
|
+
className="ml-auto text-sm underline-offset-4 hover:underline"
|
|
125
|
+
tabIndex={-1}
|
|
126
|
+
>
|
|
127
|
+
Forgot password?
|
|
128
|
+
</Link>
|
|
129
|
+
</div>
|
|
130
|
+
<Input
|
|
131
|
+
id="password"
|
|
132
|
+
type="password"
|
|
133
|
+
autoComplete="off"
|
|
134
|
+
placeholder="Enter your password"
|
|
135
|
+
aria-invalid={Boolean(form.formState.errors.password)}
|
|
136
|
+
{...form.register('password')}
|
|
137
|
+
required
|
|
138
|
+
disabled={isPending}
|
|
139
|
+
/>
|
|
140
|
+
</Field>
|
|
141
|
+
<Field className="pt-4">
|
|
142
|
+
<Button type="submit" disabled={isPending} aria-busy={isPending}>
|
|
143
|
+
{isPending && <Spinner />}
|
|
144
|
+
{isPending ? 'Signing in…' : 'Sign in'}
|
|
145
|
+
</Button>
|
|
146
|
+
</Field>
|
|
80
147
|
</div>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
85
|
-
<Input
|
|
86
|
-
id="email"
|
|
87
|
-
type="email"
|
|
88
|
-
autoComplete="email"
|
|
89
|
-
placeholder="m@example.com"
|
|
90
|
-
value={email}
|
|
91
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
92
|
-
required
|
|
93
|
-
disabled={isPending}
|
|
94
|
-
/>
|
|
95
|
-
</Field>
|
|
96
|
-
<Field>
|
|
97
|
-
<div className="flex items-center">
|
|
98
|
-
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
99
|
-
<Link
|
|
100
|
-
href="/admin/forgot-password"
|
|
101
|
-
className="ml-auto text-sm underline-offset-4 hover:underline"
|
|
102
|
-
tabIndex={-1}
|
|
103
|
-
>
|
|
104
|
-
Forgot password?
|
|
105
|
-
</Link>
|
|
106
|
-
</div>
|
|
107
|
-
<Input
|
|
108
|
-
id="password"
|
|
109
|
-
type="password"
|
|
110
|
-
autoComplete="current-password"
|
|
111
|
-
placeholder="Enter your password"
|
|
112
|
-
value={password}
|
|
113
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
114
|
-
required
|
|
115
|
-
disabled={isPending}
|
|
116
|
-
/>
|
|
117
|
-
</Field>
|
|
118
|
-
<Field className="pt-4">
|
|
119
|
-
<Button type="submit" disabled={isPending} aria-busy={isPending}>
|
|
120
|
-
{isPending && <Spinner />}
|
|
121
|
-
{isPending ? 'Signing in…' : 'Sign in'}
|
|
122
|
-
</Button>
|
|
123
|
-
</Field>
|
|
124
|
-
</div>
|
|
125
|
-
</FieldGroup>
|
|
126
|
-
</form>
|
|
148
|
+
</FieldGroup>
|
|
149
|
+
</form>
|
|
150
|
+
</Form>
|
|
127
151
|
</CardContent>
|
|
128
152
|
</Card>
|
|
129
153
|
</div>
|
|
@@ -90,7 +90,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
90
90
|
newPassword: '',
|
|
91
91
|
confirmPassword: ''
|
|
92
92
|
},
|
|
93
|
-
mode: '
|
|
93
|
+
mode: 'onChange',
|
|
94
94
|
reValidateMode: 'onChange'
|
|
95
95
|
})
|
|
96
96
|
|
|
@@ -195,7 +195,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
195
195
|
return (
|
|
196
196
|
<div className="space-y-6">
|
|
197
197
|
<Form {...profileForm}>
|
|
198
|
-
<form onSubmit={profileForm.handleSubmit(onProfileSubmit)}>
|
|
198
|
+
<form autoComplete="off" onSubmit={profileForm.handleSubmit(onProfileSubmit)}>
|
|
199
199
|
<Card>
|
|
200
200
|
<CardHeader>
|
|
201
201
|
<div className="flex items-center gap-4 pl-1">
|
|
@@ -282,7 +282,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
282
282
|
<Input
|
|
283
283
|
type="password"
|
|
284
284
|
placeholder="Enter current password to change email"
|
|
285
|
-
autoComplete="
|
|
285
|
+
autoComplete="off"
|
|
286
286
|
disabled={profilePending}
|
|
287
287
|
{...formField}
|
|
288
288
|
/>
|
|
@@ -309,7 +309,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
309
309
|
</Form>
|
|
310
310
|
|
|
311
311
|
<Form {...passwordForm}>
|
|
312
|
-
<form onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}>
|
|
312
|
+
<form autoComplete="off" onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}>
|
|
313
313
|
<Card>
|
|
314
314
|
<CardHeader>
|
|
315
315
|
<CardTitle>Change Password</CardTitle>
|
|
@@ -325,7 +325,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
325
325
|
<FormControl>
|
|
326
326
|
<Input
|
|
327
327
|
type="password"
|
|
328
|
-
autoComplete="
|
|
328
|
+
autoComplete="off"
|
|
329
329
|
disabled={passwordPending}
|
|
330
330
|
{...field}
|
|
331
331
|
/>
|
|
@@ -343,7 +343,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
343
343
|
<FormControl>
|
|
344
344
|
<Input
|
|
345
345
|
type="password"
|
|
346
|
-
autoComplete="
|
|
346
|
+
autoComplete="off"
|
|
347
347
|
disabled={passwordPending}
|
|
348
348
|
{...field}
|
|
349
349
|
/>
|
|
@@ -362,7 +362,7 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
362
362
|
<FormControl>
|
|
363
363
|
<Input
|
|
364
364
|
type="password"
|
|
365
|
-
autoComplete="
|
|
365
|
+
autoComplete="off"
|
|
366
366
|
disabled={passwordPending}
|
|
367
367
|
{...field}
|
|
368
368
|
/>
|
|
@@ -1,30 +1,53 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import type { FieldErrors } from 'react-hook-form'
|
|
4
5
|
import { authClient } from '@admin/auth/client'
|
|
5
6
|
import { Button } from '@admin/components/ui/button'
|
|
6
7
|
import { Card, CardContent } from '@admin/components/ui/card'
|
|
7
8
|
import { Field, FieldGroup, FieldLabel } from '@admin/components/ui/field'
|
|
9
|
+
import { Form } from '@admin/components/ui/form'
|
|
8
10
|
import { Input } from '@admin/components/ui/input'
|
|
9
11
|
import { Spinner } from '@admin/components/ui/spinner'
|
|
10
12
|
import { cn } from '@admin/utils/shared/cn'
|
|
13
|
+
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
|
|
11
14
|
import Image from 'next/image'
|
|
12
15
|
import Link from 'next/link'
|
|
13
16
|
import { useRouter } from 'next/navigation'
|
|
14
17
|
import { createSerializer, parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
|
|
18
|
+
import { useForm } from 'react-hook-form'
|
|
19
|
+
import { toast } from 'sonner'
|
|
20
|
+
import { z } from 'zod/v3'
|
|
15
21
|
|
|
16
22
|
const serializeLoginUrl = createSerializer({
|
|
17
23
|
reset: parseAsStringLiteral(['success'])
|
|
18
24
|
})
|
|
19
25
|
|
|
26
|
+
const resetPasswordSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
|
29
|
+
confirmPassword: z.string().min(1, 'Confirm the new password')
|
|
30
|
+
})
|
|
31
|
+
.refine((values) => values.newPassword === values.confirmPassword, {
|
|
32
|
+
message: 'Passwords do not match',
|
|
33
|
+
path: ['confirmPassword']
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
type ResetPasswordFormValues = z.infer<typeof resetPasswordSchema>
|
|
37
|
+
|
|
20
38
|
export function ResetPasswordForm({ className, ...props }: React.ComponentProps<'div'>) {
|
|
21
39
|
const router = useRouter()
|
|
22
40
|
const [token] = useQueryState('token', parseAsString)
|
|
23
|
-
|
|
24
|
-
const [newPassword, setNewPassword] = React.useState('')
|
|
25
|
-
const [confirmPassword, setConfirmPassword] = React.useState('')
|
|
26
|
-
const [error, setError] = React.useState<string | null>(null)
|
|
27
41
|
const [isPending, startTransition] = React.useTransition()
|
|
42
|
+
const form = useForm<ResetPasswordFormValues>({
|
|
43
|
+
resolver: standardSchemaResolver(resetPasswordSchema),
|
|
44
|
+
defaultValues: {
|
|
45
|
+
newPassword: '',
|
|
46
|
+
confirmPassword: ''
|
|
47
|
+
},
|
|
48
|
+
mode: 'onChange',
|
|
49
|
+
reValidateMode: 'onChange'
|
|
50
|
+
})
|
|
28
51
|
|
|
29
52
|
if (!token) {
|
|
30
53
|
return (
|
|
@@ -37,7 +60,7 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
|
|
|
37
60
|
alt=""
|
|
38
61
|
fill
|
|
39
62
|
sizes="(min-width: 768px) 42vw, 0px"
|
|
40
|
-
className="object-cover
|
|
63
|
+
className="object-cover"
|
|
41
64
|
/>
|
|
42
65
|
</div>
|
|
43
66
|
<div className="p-6 md:p-20">
|
|
@@ -62,38 +85,30 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
|
|
|
62
85
|
)
|
|
63
86
|
}
|
|
64
87
|
|
|
65
|
-
const handleSubmit = (
|
|
66
|
-
e.preventDefault()
|
|
67
|
-
setError(null)
|
|
68
|
-
|
|
69
|
-
if (newPassword !== confirmPassword) {
|
|
70
|
-
setError('Passwords do not match')
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (newPassword.length < 8) {
|
|
75
|
-
setError('Password must be at least 8 characters')
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
|
|
88
|
+
const handleSubmit = (values: ResetPasswordFormValues) => {
|
|
79
89
|
startTransition(async () => {
|
|
80
90
|
try {
|
|
81
91
|
const { error } = await authClient.resetPassword({
|
|
82
|
-
newPassword,
|
|
92
|
+
newPassword: values.newPassword,
|
|
83
93
|
token
|
|
84
94
|
})
|
|
85
95
|
if (error) {
|
|
86
|
-
|
|
96
|
+
toast.error(error.message || 'Failed to reset password')
|
|
87
97
|
return
|
|
88
98
|
}
|
|
89
99
|
await authClient.getSession({ query: { disableCookieCache: true } })
|
|
90
100
|
router.push(serializeLoginUrl('/admin/login', { reset: 'success' }))
|
|
91
101
|
} catch {
|
|
92
|
-
|
|
102
|
+
toast.error('Failed to reset your password. Try again.')
|
|
93
103
|
}
|
|
94
104
|
})
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
const handleInvalid = (errors: FieldErrors<ResetPasswordFormValues>) => {
|
|
108
|
+
const message = errors.newPassword?.message ?? errors.confirmPassword?.message
|
|
109
|
+
toast.error(typeof message === 'string' ? message : 'Please check your new password.')
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
return (
|
|
98
113
|
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
|
99
114
|
<Card className="overflow-hidden p-0">
|
|
@@ -104,56 +119,60 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
|
|
|
104
119
|
alt=""
|
|
105
120
|
fill
|
|
106
121
|
sizes="(min-width: 768px) 42vw, 0px"
|
|
107
|
-
className="object-cover
|
|
122
|
+
className="object-cover"
|
|
108
123
|
/>
|
|
109
124
|
</div>
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
<Form {...form}>
|
|
126
|
+
<form
|
|
127
|
+
autoComplete="off"
|
|
128
|
+
noValidate
|
|
129
|
+
onSubmit={form.handleSubmit(handleSubmit, handleInvalid)}
|
|
130
|
+
className="p-6 md:p-20"
|
|
131
|
+
>
|
|
132
|
+
<FieldGroup className="gap-10 pb-12">
|
|
133
|
+
<div className="flex flex-col items-start">
|
|
134
|
+
<h1 className="text-xl font-medium">Reset password</h1>
|
|
135
|
+
<p className="text-balance text-muted-foreground">
|
|
136
|
+
Enter your new password below
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="flex flex-col gap-4">
|
|
140
|
+
<Field>
|
|
141
|
+
<FieldLabel htmlFor="newPassword">New Password</FieldLabel>
|
|
142
|
+
<Input
|
|
143
|
+
id="newPassword"
|
|
144
|
+
type="password"
|
|
145
|
+
autoComplete="off"
|
|
146
|
+
placeholder="Enter new password"
|
|
147
|
+
aria-invalid={Boolean(form.formState.errors.newPassword)}
|
|
148
|
+
{...form.register('newPassword')}
|
|
149
|
+
required
|
|
150
|
+
disabled={isPending}
|
|
151
|
+
/>
|
|
152
|
+
</Field>
|
|
153
|
+
<Field>
|
|
154
|
+
<FieldLabel htmlFor="confirmPassword">Confirm Password</FieldLabel>
|
|
155
|
+
<Input
|
|
156
|
+
id="confirmPassword"
|
|
157
|
+
type="password"
|
|
158
|
+
autoComplete="off"
|
|
159
|
+
placeholder="Confirm new password"
|
|
160
|
+
aria-invalid={Boolean(form.formState.errors.confirmPassword)}
|
|
161
|
+
{...form.register('confirmPassword')}
|
|
162
|
+
required
|
|
163
|
+
disabled={isPending}
|
|
164
|
+
/>
|
|
165
|
+
</Field>
|
|
166
|
+
<Field className="pt-4">
|
|
167
|
+
<Button type="submit" disabled={isPending} aria-busy={isPending}>
|
|
168
|
+
{isPending && <Spinner />}
|
|
169
|
+
{isPending ? 'Resetting…' : 'Reset Password'}
|
|
170
|
+
</Button>
|
|
171
|
+
</Field>
|
|
119
172
|
</div>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<FieldLabel htmlFor="newPassword">New Password</FieldLabel>
|
|
124
|
-
<Input
|
|
125
|
-
id="newPassword"
|
|
126
|
-
type="password"
|
|
127
|
-
autoComplete="new-password"
|
|
128
|
-
placeholder="Enter new password"
|
|
129
|
-
value={newPassword}
|
|
130
|
-
onChange={(e) => setNewPassword(e.target.value)}
|
|
131
|
-
required
|
|
132
|
-
disabled={isPending}
|
|
133
|
-
/>
|
|
134
|
-
</Field>
|
|
135
|
-
<Field>
|
|
136
|
-
<FieldLabel htmlFor="confirmPassword">Confirm Password</FieldLabel>
|
|
137
|
-
<Input
|
|
138
|
-
id="confirmPassword"
|
|
139
|
-
type="password"
|
|
140
|
-
autoComplete="new-password"
|
|
141
|
-
placeholder="Confirm new password"
|
|
142
|
-
value={confirmPassword}
|
|
143
|
-
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
144
|
-
required
|
|
145
|
-
disabled={isPending}
|
|
146
|
-
/>
|
|
147
|
-
</Field>
|
|
148
|
-
<Field className="pt-4">
|
|
149
|
-
<Button type="submit" disabled={isPending} aria-busy={isPending}>
|
|
150
|
-
{isPending && <Spinner />}
|
|
151
|
-
{isPending ? 'Resetting…' : 'Reset Password'}
|
|
152
|
-
</Button>
|
|
153
|
-
</Field>
|
|
154
|
-
</div>
|
|
155
|
-
</FieldGroup>
|
|
156
|
-
</form>
|
|
173
|
+
</FieldGroup>
|
|
174
|
+
</form>
|
|
175
|
+
</Form>
|
|
157
176
|
</CardContent>
|
|
158
177
|
</Card>
|
|
159
178
|
</div>
|
package/dist/assets/adapters/next/templates/init/pages/settings/forms/form-notifications-drawer.tsx
CHANGED
|
@@ -86,6 +86,7 @@ export function FormNotificationsDrawer({
|
|
|
86
86
|
<DrawerContent className="h-full sm:max-w-xl">
|
|
87
87
|
<Form {...form}>
|
|
88
88
|
<form
|
|
89
|
+
autoComplete="off"
|
|
89
90
|
onSubmit={form.handleSubmit((values) => mutation.mutate(values))}
|
|
90
91
|
className="flex min-h-0 flex-1 flex-col gap-4"
|
|
91
92
|
>
|