@betterstart/cli 0.1.80 → 0.1.82
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/cli.js +15 -2
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/init/components/layout/cms-search.tsx +6 -5
- package/templates/init/components/layout/cms-sidebar.tsx +3 -7
- package/templates/init/data/navigation.ts +6 -6
- package/templates/init/lib/actions/profile.ts +4 -0
- package/templates/init/lib/auth/auth.ts +17 -0
- package/templates/init/pages/forgot-password-form.tsx +130 -0
- package/templates/init/pages/forgot-password-page.tsx +20 -0
- package/templates/init/pages/login-form.tsx +17 -1
- package/templates/init/pages/login-page.tsx +12 -2
- package/templates/init/pages/profile/profile-form.tsx +67 -137
- package/templates/init/pages/profile/profile-page.tsx +1 -1
- package/templates/init/pages/reset-password-form.tsx +159 -0
- package/templates/init/pages/reset-password-page.tsx +20 -0
- package/templates/ui/button.tsx +1 -1
- package/templates/ui/card.tsx +2 -2
- package/templates/ui/curriculum-editor.tsx +2 -2
- package/templates/ui/image-upload-field.tsx +2 -2
- package/templates/ui/input.tsx +5 -4
- package/templates/ui/media-upload-field.tsx +2 -2
- package/templates/ui/placeholder.tsx +1 -1
- package/templates/ui/sidebar.tsx +1 -1
- package/templates/ui/video-upload-field.tsx +2 -2
package/package.json
CHANGED
|
@@ -3,17 +3,18 @@ import { Command, Search } from 'lucide-react'
|
|
|
3
3
|
|
|
4
4
|
export const CmsSearch = () => {
|
|
5
5
|
return (
|
|
6
|
-
<div className="flex items-center gap-2 relative w-full max-w-
|
|
6
|
+
<div className="flex items-center gap-2 relative w-full max-w-60">
|
|
7
7
|
<Button
|
|
8
8
|
variant="outline"
|
|
9
|
-
|
|
9
|
+
size="sm"
|
|
10
|
+
className="w-full text-left items-center pr-2! pl-3.5 py-0 rounded-full bg-white"
|
|
10
11
|
>
|
|
11
|
-
<Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground
|
|
12
|
-
<span className="w-full font-normal text-sm text-muted-foreground
|
|
12
|
+
<Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground" />
|
|
13
|
+
<span className="w-full font-normal text-sm text-muted-foreground [text-box-trim:trim-both]">
|
|
13
14
|
Search
|
|
14
15
|
</span>
|
|
15
16
|
<div className="flex items-center gap-1 py-0.5 border rounded-full corner-squircle px-2 border-border bg-background">
|
|
16
|
-
<Command className="size-3
|
|
17
|
+
<Command className="size-3 text-muted-foreground" />
|
|
17
18
|
<span className="font-mono text-xs font-medium">K</span>
|
|
18
19
|
</div>
|
|
19
20
|
</Button>
|
|
@@ -56,9 +56,6 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
|
56
56
|
const user = session?.user ?? null
|
|
57
57
|
const groups = groupNavItems(cmsNavigation)
|
|
58
58
|
|
|
59
|
-
console.log({ settings });
|
|
60
|
-
|
|
61
|
-
|
|
62
59
|
return (
|
|
63
60
|
<Sidebar collapsible="icon" {...props}>
|
|
64
61
|
<SidebarHeader className="border-b border-border h-14 items-center flex w-full">
|
|
@@ -79,7 +76,7 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
|
79
76
|
</div>
|
|
80
77
|
</SidebarHeader>
|
|
81
78
|
<SidebarContent className='pb-2'>
|
|
82
|
-
<SidebarGroup className="pb-
|
|
79
|
+
<SidebarGroup className="pb-0">
|
|
83
80
|
<CmsSearch />
|
|
84
81
|
</SidebarGroup>
|
|
85
82
|
|
|
@@ -141,12 +138,11 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
|
141
138
|
</div>
|
|
142
139
|
|
|
143
140
|
<div className="group-data-[collapsible=icon]:hidden">
|
|
144
|
-
<span className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'rounded-full group-hover:bg-white size-
|
|
145
|
-
<Ellipsis strokeWidth={2} />
|
|
141
|
+
<span className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'rounded-full group-hover:bg-white size-7')}>
|
|
142
|
+
<Ellipsis strokeWidth={2} className='size-4' />
|
|
146
143
|
</span>
|
|
147
144
|
</div>
|
|
148
145
|
</Button>
|
|
149
|
-
|
|
150
146
|
</DropdownMenuTrigger>
|
|
151
147
|
<DropdownMenuContent align="start" className="w-68 p-0">
|
|
152
148
|
<DropdownMenuGroup className='py-2 border-b border-border px-2'>
|
|
@@ -24,16 +24,16 @@ export const cmsNavigation: CmsNavigationItem[] = [
|
|
|
24
24
|
href: '/cms/media',
|
|
25
25
|
icon: ImagePlay,
|
|
26
26
|
},
|
|
27
|
-
{
|
|
28
|
-
label: 'Categories',
|
|
29
|
-
href: '/cms/categories',
|
|
30
|
-
icon: Tag,
|
|
31
|
-
group: 'Blog',
|
|
32
|
-
},
|
|
33
27
|
{
|
|
34
28
|
label: 'Posts',
|
|
35
29
|
href: '/cms/posts',
|
|
36
30
|
icon: FileText,
|
|
37
31
|
group: 'Blog',
|
|
38
32
|
},
|
|
33
|
+
{
|
|
34
|
+
label: 'Categories',
|
|
35
|
+
href: '/cms/categories',
|
|
36
|
+
icon: Tag,
|
|
37
|
+
group: 'Blog',
|
|
38
|
+
},
|
|
39
39
|
]
|
|
@@ -6,6 +6,10 @@ import db from '@cms/db'
|
|
|
6
6
|
import { user } from '@cms/db/schema'
|
|
7
7
|
import { eq } from 'drizzle-orm'
|
|
8
8
|
|
|
9
|
+
export async function isEmailConfigured(): Promise<boolean> {
|
|
10
|
+
return !!process.env.BETTERSTART_RESEND_API_KEY
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
export interface UpdateEmailResult {
|
|
10
14
|
success: boolean
|
|
11
15
|
error?: string
|
|
@@ -3,6 +3,7 @@ import * as schema from '@cms/db/schema'
|
|
|
3
3
|
import { betterAuth } from 'better-auth'
|
|
4
4
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
5
5
|
import { toNextJsHandler } from 'better-auth/next-js'
|
|
6
|
+
import { Resend } from 'resend'
|
|
6
7
|
|
|
7
8
|
export { toNextJsHandler }
|
|
8
9
|
|
|
@@ -22,6 +23,22 @@ export const auth = betterAuth({
|
|
|
22
23
|
emailAndPassword: {
|
|
23
24
|
enabled: true,
|
|
24
25
|
minPasswordLength: 8,
|
|
26
|
+
revokeSessionsOnPasswordReset: true,
|
|
27
|
+
sendResetPassword: async ({ user, url }: { user: { email: string; name: string }; url: string }) => {
|
|
28
|
+
const apiKey = process.env.BETTERSTART_RESEND_API_KEY
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Email is not configured. Set BETTERSTART_RESEND_API_KEY to enable password reset emails.'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
const resend = new Resend(apiKey)
|
|
35
|
+
await resend.emails.send({
|
|
36
|
+
from: process.env.BETTERSTART_EMAIL_FROM || 'noreply@yourdomain.com',
|
|
37
|
+
to: user.email,
|
|
38
|
+
subject: 'Reset your password',
|
|
39
|
+
html: `<p>Hi ${user.name},</p><p>Click the link below to reset your password:</p><p><a href="${url}">Reset Password</a></p><p>This link will expire in 1 hour. If you didn't request this, you can safely ignore this email.</p>`,
|
|
40
|
+
})
|
|
41
|
+
},
|
|
25
42
|
},
|
|
26
43
|
session: {
|
|
27
44
|
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { isEmailConfigured } from '@cms/actions/profile'
|
|
4
|
+
import { authClient } from '@cms/auth/client'
|
|
5
|
+
import { Button } from '@cms/components/ui/button'
|
|
6
|
+
import { Card, CardContent } from '@cms/components/ui/card'
|
|
7
|
+
import { Field, FieldGroup, FieldLabel } from '@cms/components/ui/field'
|
|
8
|
+
import { Input } from '@cms/components/ui/input'
|
|
9
|
+
import { cn } from '@cms/utils/cn'
|
|
10
|
+
import { LoaderCircle } from 'lucide-react'
|
|
11
|
+
import Link from 'next/link'
|
|
12
|
+
import * as React from 'react'
|
|
13
|
+
|
|
14
|
+
export function ForgotPasswordForm({
|
|
15
|
+
className,
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<'div'>) {
|
|
18
|
+
const [email, setEmail] = React.useState('')
|
|
19
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
20
|
+
const [success, setSuccess] = React.useState(false)
|
|
21
|
+
const [isPending, startTransition] = React.useTransition()
|
|
22
|
+
|
|
23
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
24
|
+
e.preventDefault()
|
|
25
|
+
setError(null)
|
|
26
|
+
|
|
27
|
+
startTransition(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const emailReady = await isEmailConfigured()
|
|
30
|
+
if (!emailReady) {
|
|
31
|
+
setError(
|
|
32
|
+
'Email is not configured. Set BETTERSTART_RESEND_API_KEY to enable password reset emails.'
|
|
33
|
+
)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
const { error } = await authClient.requestPasswordReset({
|
|
37
|
+
email,
|
|
38
|
+
redirectTo: '/cms/reset-password',
|
|
39
|
+
})
|
|
40
|
+
if (error) {
|
|
41
|
+
setError(error.message || 'Failed to send reset link')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
setSuccess(true)
|
|
45
|
+
} catch {
|
|
46
|
+
setError('An error occurred. Please try again.')
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
|
53
|
+
<Card className="overflow-hidden p-0">
|
|
54
|
+
<CardContent className="grid p-0 md:grid-cols-[5fr_7fr]">
|
|
55
|
+
<div className="relative hidden bg-muted md:block">
|
|
56
|
+
<img
|
|
57
|
+
src="https://assets.betterstart.dev/assets/placeholder.png"
|
|
58
|
+
alt="Image"
|
|
59
|
+
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="p-6 md:p-20">
|
|
63
|
+
{success ? (
|
|
64
|
+
<FieldGroup className="pb-12 gap-10">
|
|
65
|
+
<div className="flex flex-col items-start">
|
|
66
|
+
<h1 className="text-xl font-medium">Check your email</h1>
|
|
67
|
+
<p className="text-balance text-muted-foreground">
|
|
68
|
+
If an account exists for {email}, we sent a password reset
|
|
69
|
+
link. Check your inbox and spam folder.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
<Link
|
|
73
|
+
href="/cms/login"
|
|
74
|
+
className="text-sm underline-offset-4 hover:underline"
|
|
75
|
+
>
|
|
76
|
+
Back to login
|
|
77
|
+
</Link>
|
|
78
|
+
</FieldGroup>
|
|
79
|
+
) : (
|
|
80
|
+
<form onSubmit={handleSubmit}>
|
|
81
|
+
<FieldGroup className="pb-12 gap-10">
|
|
82
|
+
<div className="flex flex-col items-start">
|
|
83
|
+
<h1 className="text-xl font-medium">Forgot password</h1>
|
|
84
|
+
<p className="text-balance text-muted-foreground">
|
|
85
|
+
Enter your email and we'll send you a reset link
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
{error ? (
|
|
89
|
+
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
|
|
90
|
+
{error}
|
|
91
|
+
</div>
|
|
92
|
+
) : null}
|
|
93
|
+
<div className="flex flex-col gap-4">
|
|
94
|
+
<Field>
|
|
95
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
96
|
+
<Input
|
|
97
|
+
id="email"
|
|
98
|
+
type="email"
|
|
99
|
+
autoComplete="email"
|
|
100
|
+
placeholder="m@example.com"
|
|
101
|
+
value={email}
|
|
102
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
103
|
+
required
|
|
104
|
+
disabled={isPending}
|
|
105
|
+
/>
|
|
106
|
+
</Field>
|
|
107
|
+
<Field className="pt-4">
|
|
108
|
+
<Button type="submit" disabled={isPending}>
|
|
109
|
+
{isPending && (
|
|
110
|
+
<LoaderCircle className="animate-spin" />
|
|
111
|
+
)}
|
|
112
|
+
{isPending ? 'Sending...' : 'Send Reset Link'}
|
|
113
|
+
</Button>
|
|
114
|
+
</Field>
|
|
115
|
+
<Link
|
|
116
|
+
href="/cms/login"
|
|
117
|
+
className="text-sm text-center underline-offset-4 hover:underline"
|
|
118
|
+
>
|
|
119
|
+
Back to login
|
|
120
|
+
</Link>
|
|
121
|
+
</div>
|
|
122
|
+
</FieldGroup>
|
|
123
|
+
</form>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</CardContent>
|
|
127
|
+
</Card>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { Suspense } from 'react'
|
|
3
|
+
import { ForgotPasswordForm } from './forgot-password-form'
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: 'Forgot Password',
|
|
7
|
+
robots: { index: false, follow: false },
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function ForgotPasswordPage() {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex min-h-svh flex-col items-center justify-center bg-background p-6 md:p-10">
|
|
13
|
+
<div className="w-full max-w-sm md:max-w-4xl">
|
|
14
|
+
<Suspense>
|
|
15
|
+
<ForgotPasswordForm />
|
|
16
|
+
</Suspense>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
import { Input } from '@cms/components/ui/input'
|
|
12
12
|
import { cn } from '@cms/utils/cn'
|
|
13
13
|
import { LoaderCircle } from 'lucide-react'
|
|
14
|
-
import
|
|
14
|
+
import Link from 'next/link'
|
|
15
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
15
16
|
import * as React from 'react'
|
|
16
17
|
|
|
17
18
|
export function LoginForm({
|
|
@@ -19,6 +20,8 @@ export function LoginForm({
|
|
|
19
20
|
...props
|
|
20
21
|
}: React.ComponentProps<'div'>) {
|
|
21
22
|
const router = useRouter()
|
|
23
|
+
const searchParams = useSearchParams()
|
|
24
|
+
const resetSuccess = searchParams.get('reset') === 'success'
|
|
22
25
|
const [email, setEmail] = React.useState('')
|
|
23
26
|
const [password, setPassword] = React.useState('')
|
|
24
27
|
const [error, setError] = React.useState<string | null>(null)
|
|
@@ -67,6 +70,12 @@ export function LoginForm({
|
|
|
67
70
|
Login to your account
|
|
68
71
|
</p>
|
|
69
72
|
</div>
|
|
73
|
+
{resetSuccess ? (
|
|
74
|
+
<div className="bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 text-sm p-3 rounded-md">
|
|
75
|
+
Password reset successfully. Please log in with your new
|
|
76
|
+
password.
|
|
77
|
+
</div>
|
|
78
|
+
) : null}
|
|
70
79
|
{error ? (
|
|
71
80
|
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
|
|
72
81
|
{error}
|
|
@@ -89,6 +98,13 @@ export function LoginForm({
|
|
|
89
98
|
<Field>
|
|
90
99
|
<div className="flex items-center">
|
|
91
100
|
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
101
|
+
<Link
|
|
102
|
+
href="/cms/forgot-password"
|
|
103
|
+
className="ml-auto text-sm underline-offset-4 hover:underline"
|
|
104
|
+
tabIndex={-1}
|
|
105
|
+
>
|
|
106
|
+
Forgot password?
|
|
107
|
+
</Link>
|
|
92
108
|
</div>
|
|
93
109
|
<Input
|
|
94
110
|
id="password"
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
|
+
import { redirect } from 'next/navigation'
|
|
3
|
+
import { Suspense } from 'react'
|
|
4
|
+
import { getSession } from '@cms/auth/middleware'
|
|
2
5
|
import { LoginForm } from './login-form'
|
|
3
6
|
|
|
4
7
|
export const metadata: Metadata = {
|
|
@@ -6,11 +9,18 @@ export const metadata: Metadata = {
|
|
|
6
9
|
robots: { index: false, follow: false },
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
export default function LoginPage() {
|
|
12
|
+
export default async function LoginPage() {
|
|
13
|
+
const session = await getSession()
|
|
14
|
+
if (session?.user) {
|
|
15
|
+
redirect('/cms')
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
return (
|
|
11
19
|
<div className="flex min-h-svh flex-col items-center justify-center bg-background p-6 md:p-10">
|
|
12
20
|
<div className="w-full max-w-sm md:max-w-4xl">
|
|
13
|
-
<
|
|
21
|
+
<Suspense>
|
|
22
|
+
<LoginForm />
|
|
23
|
+
</Suspense>
|
|
14
24
|
</div>
|
|
15
25
|
</div>
|
|
16
26
|
)
|
|
@@ -5,10 +5,11 @@ import { useForm } from 'react-hook-form'
|
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { z } from 'zod/v3'
|
|
7
7
|
import { authClient } from '@cms/auth/client'
|
|
8
|
-
import { updateEmail } from '@cms/actions/profile'
|
|
8
|
+
import { isEmailConfigured, updateEmail } from '@cms/actions/profile'
|
|
9
9
|
import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
|
|
10
10
|
import { Button } from '@cms/components/ui/button'
|
|
11
11
|
import { Input } from '@cms/components/ui/input'
|
|
12
|
+
import { LoaderCircle } from 'lucide-react'
|
|
12
13
|
import {
|
|
13
14
|
Form,
|
|
14
15
|
FormControl,
|
|
@@ -41,19 +42,6 @@ const profileSchema = z.object({
|
|
|
41
42
|
|
|
42
43
|
type ProfileValues = z.infer<typeof profileSchema>
|
|
43
44
|
|
|
44
|
-
const passwordSchema = z
|
|
45
|
-
.object({
|
|
46
|
-
currentPassword: z.string().min(1, 'Current password is required'),
|
|
47
|
-
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
|
48
|
-
confirmPassword: z.string().min(1, 'Please confirm your new password'),
|
|
49
|
-
})
|
|
50
|
-
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
51
|
-
message: 'Passwords do not match',
|
|
52
|
-
path: ['confirmPassword'],
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
type PasswordValues = z.infer<typeof passwordSchema>
|
|
56
|
-
|
|
57
45
|
interface ProfileFormProps {
|
|
58
46
|
user: {
|
|
59
47
|
name: string
|
|
@@ -65,7 +53,7 @@ interface ProfileFormProps {
|
|
|
65
53
|
export function ProfileForm({ user }: ProfileFormProps) {
|
|
66
54
|
const router = useRouter()
|
|
67
55
|
const [profilePending, setProfilePending] = useState(false)
|
|
68
|
-
const [
|
|
56
|
+
const [resetPending, setResetPending] = useState(false)
|
|
69
57
|
|
|
70
58
|
const profileForm = useForm<ProfileValues>({
|
|
71
59
|
resolver: zodResolver(profileSchema),
|
|
@@ -77,15 +65,6 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
77
65
|
},
|
|
78
66
|
})
|
|
79
67
|
|
|
80
|
-
const passwordForm = useForm<PasswordValues>({
|
|
81
|
-
resolver: zodResolver(passwordSchema),
|
|
82
|
-
defaultValues: {
|
|
83
|
-
currentPassword: '',
|
|
84
|
-
newPassword: '',
|
|
85
|
-
confirmPassword: '',
|
|
86
|
-
},
|
|
87
|
-
})
|
|
88
|
-
|
|
89
68
|
const emailDirty = profileForm.watch('email') !== user.email
|
|
90
69
|
const imageValue = profileForm.watch('image')
|
|
91
70
|
|
|
@@ -140,27 +119,29 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
140
119
|
}
|
|
141
120
|
}
|
|
142
121
|
|
|
143
|
-
async function
|
|
144
|
-
|
|
145
|
-
|
|
122
|
+
async function handleResetPassword() {
|
|
123
|
+
setResetPending(true)
|
|
146
124
|
try {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
125
|
+
const emailReady = await isEmailConfigured()
|
|
126
|
+
if (!emailReady) {
|
|
127
|
+
toast.error(
|
|
128
|
+
'Email is not configured. Set BETTERSTART_RESEND_API_KEY to enable password reset emails.'
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
const { error } = await authClient.requestPasswordReset({
|
|
133
|
+
email: user.email,
|
|
134
|
+
redirectTo: '/cms/reset-password',
|
|
150
135
|
})
|
|
151
|
-
|
|
152
136
|
if (error) {
|
|
153
|
-
toast.error(error.message || 'Failed to
|
|
154
|
-
setPasswordPending(false)
|
|
137
|
+
toast.error(error.message || 'Failed to send reset link')
|
|
155
138
|
return
|
|
156
139
|
}
|
|
157
|
-
|
|
158
|
-
toast.success('Password changed successfully')
|
|
159
|
-
passwordForm.reset()
|
|
140
|
+
toast.success('Check your email for a password reset link')
|
|
160
141
|
} catch {
|
|
161
|
-
toast.error('An error occurred while
|
|
142
|
+
toast.error('An error occurred while sending the reset link')
|
|
162
143
|
} finally {
|
|
163
|
-
|
|
144
|
+
setResetPending(false)
|
|
164
145
|
}
|
|
165
146
|
}
|
|
166
147
|
|
|
@@ -186,6 +167,27 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
186
167
|
</div>
|
|
187
168
|
</CardHeader>
|
|
188
169
|
<CardContent className="space-y-6">
|
|
170
|
+
<FormField
|
|
171
|
+
control={profileForm.control}
|
|
172
|
+
name="image"
|
|
173
|
+
render={({ field: formField }) => (
|
|
174
|
+
<FormItem>
|
|
175
|
+
<FormLabel>Profile Picture</FormLabel>
|
|
176
|
+
<FormControl>
|
|
177
|
+
<ImageUploadField
|
|
178
|
+
value={formField.value}
|
|
179
|
+
onChange={formField.onChange}
|
|
180
|
+
onBlur={formField.onBlur}
|
|
181
|
+
disabled={profilePending}
|
|
182
|
+
maxSizeInMB={5}
|
|
183
|
+
label=""
|
|
184
|
+
/>
|
|
185
|
+
</FormControl>
|
|
186
|
+
<FormMessage />
|
|
187
|
+
</FormItem>
|
|
188
|
+
)}
|
|
189
|
+
/>
|
|
190
|
+
|
|
189
191
|
<FormField
|
|
190
192
|
control={profileForm.control}
|
|
191
193
|
name="name"
|
|
@@ -245,30 +247,10 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
245
247
|
)}
|
|
246
248
|
/>
|
|
247
249
|
)}
|
|
248
|
-
|
|
249
|
-
<FormField
|
|
250
|
-
control={profileForm.control}
|
|
251
|
-
name="image"
|
|
252
|
-
render={({ field: formField }) => (
|
|
253
|
-
<FormItem>
|
|
254
|
-
<FormLabel>Profile Picture</FormLabel>
|
|
255
|
-
<FormControl>
|
|
256
|
-
<ImageUploadField
|
|
257
|
-
value={formField.value}
|
|
258
|
-
onChange={formField.onChange}
|
|
259
|
-
onBlur={formField.onBlur}
|
|
260
|
-
disabled={profilePending}
|
|
261
|
-
maxSizeInMB={5}
|
|
262
|
-
label=""
|
|
263
|
-
/>
|
|
264
|
-
</FormControl>
|
|
265
|
-
<FormMessage />
|
|
266
|
-
</FormItem>
|
|
267
|
-
)}
|
|
268
|
-
/>
|
|
269
250
|
</CardContent>
|
|
270
251
|
<CardFooter>
|
|
271
252
|
<Button type="submit" disabled={profilePending || !profileForm.formState.isDirty} size="sm" className="ml-auto min-w-25">
|
|
253
|
+
{profilePending && <LoaderCircle className="animate-spin" />}
|
|
272
254
|
{profilePending ? 'Saving...' : 'Save'}
|
|
273
255
|
</Button>
|
|
274
256
|
</CardFooter>
|
|
@@ -276,84 +258,32 @@ export function ProfileForm({ user }: ProfileFormProps) {
|
|
|
276
258
|
</form>
|
|
277
259
|
</Form>
|
|
278
260
|
|
|
279
|
-
<
|
|
280
|
-
<
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
</FormItem>
|
|
306
|
-
)}
|
|
307
|
-
/>
|
|
308
|
-
|
|
309
|
-
<FormField
|
|
310
|
-
control={passwordForm.control}
|
|
311
|
-
name="newPassword"
|
|
312
|
-
render={({ field: formField }) => (
|
|
313
|
-
<FormItem>
|
|
314
|
-
<FormLabel>New Password</FormLabel>
|
|
315
|
-
<FormControl>
|
|
316
|
-
<Input
|
|
317
|
-
type="password"
|
|
318
|
-
placeholder="Enter new password"
|
|
319
|
-
autoComplete="new-password"
|
|
320
|
-
disabled={passwordPending}
|
|
321
|
-
{...formField}
|
|
322
|
-
/>
|
|
323
|
-
</FormControl>
|
|
324
|
-
<FormMessage />
|
|
325
|
-
</FormItem>
|
|
326
|
-
)}
|
|
327
|
-
/>
|
|
328
|
-
|
|
329
|
-
<FormField
|
|
330
|
-
control={passwordForm.control}
|
|
331
|
-
name="confirmPassword"
|
|
332
|
-
render={({ field: formField }) => (
|
|
333
|
-
<FormItem>
|
|
334
|
-
<FormLabel>Confirm New Password</FormLabel>
|
|
335
|
-
<FormControl>
|
|
336
|
-
<Input
|
|
337
|
-
type="password"
|
|
338
|
-
placeholder="Confirm new password"
|
|
339
|
-
autoComplete="new-password"
|
|
340
|
-
disabled={passwordPending}
|
|
341
|
-
{...formField}
|
|
342
|
-
/>
|
|
343
|
-
</FormControl>
|
|
344
|
-
<FormMessage />
|
|
345
|
-
</FormItem>
|
|
346
|
-
)}
|
|
347
|
-
/>
|
|
348
|
-
</CardContent>
|
|
349
|
-
<CardFooter>
|
|
350
|
-
<Button type="submit" disabled={passwordPending || !passwordForm.formState.isDirty} size="sm" className="ml-auto min-w-25">
|
|
351
|
-
{passwordPending ? 'Changing...' : 'Change Password'}
|
|
352
|
-
</Button>
|
|
353
|
-
</CardFooter>
|
|
354
|
-
</Card>
|
|
355
|
-
</form>
|
|
356
|
-
</Form>
|
|
261
|
+
<Card className="material-sm!">
|
|
262
|
+
<CardHeader>
|
|
263
|
+
<CardTitle>Reset Password</CardTitle>
|
|
264
|
+
<CardDescription>
|
|
265
|
+
Send a password reset link to your email address
|
|
266
|
+
</CardDescription>
|
|
267
|
+
</CardHeader>
|
|
268
|
+
<CardContent>
|
|
269
|
+
<p className="text-sm text-muted-foreground">
|
|
270
|
+
A reset link will be sent to{' '}
|
|
271
|
+
<span className="font-medium text-foreground">{user.email}</span>
|
|
272
|
+
</p>
|
|
273
|
+
</CardContent>
|
|
274
|
+
<CardFooter>
|
|
275
|
+
<Button
|
|
276
|
+
type="button"
|
|
277
|
+
size="sm"
|
|
278
|
+
className="ml-auto min-w-25"
|
|
279
|
+
disabled={resetPending}
|
|
280
|
+
onClick={handleResetPassword}
|
|
281
|
+
>
|
|
282
|
+
{resetPending && <LoaderCircle className="animate-spin" />}
|
|
283
|
+
{resetPending ? 'Sending...' : 'Reset Password'}
|
|
284
|
+
</Button>
|
|
285
|
+
</CardFooter>
|
|
286
|
+
</Card>
|
|
357
287
|
</div>
|
|
358
288
|
)
|
|
359
289
|
}
|
|
@@ -20,7 +20,7 @@ export default async function ProfilePage() {
|
|
|
20
20
|
return (
|
|
21
21
|
<>
|
|
22
22
|
{showPageHeader && <PageHeader title="Profile" />}
|
|
23
|
-
<main className="container mx-auto max-w-
|
|
23
|
+
<main className="container mx-auto max-w-5xl pt-10 pb-20 w-full">
|
|
24
24
|
<ProfileForm
|
|
25
25
|
user={{
|
|
26
26
|
name: session.user.name,
|