[READ-ONLY] One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links 馃搮 calendar.xyehr.cn
nextjs
0

Configure Feed

Select the types of activity you want to include in your feed.

One-Calendar / components / auth / reset-form.tsx
6.9 kB 198 lines
1'use client' 2 3import { Turnstile } from '@marsidev/react-turnstile' 4import { useSearchParams } from 'next/navigation' 5import { useState } from 'react' 6import type React from 'react' 7 8import { Button } from '@/components/ui/button' 9import { 10 Card, 11 CardContent, 12 CardDescription, 13 CardHeader, 14 CardTitle, 15} from '@/components/ui/card' 16import { Input } from '@/components/ui/input' 17import { Label } from '@/components/ui/label' 18import { authClient } from '@/lib/auth/client' 19import { cn } from '@/lib/utils' 20 21export function ResetPasswordForm({ 22 className, 23 ...props 24}: React.ComponentPropsWithoutRef<'div'>) { 25 const searchParams = useSearchParams() 26 const token = searchParams.get('token') 27 const [email, setEmail] = useState('') 28 const [password, setPassword] = useState('') 29 const [confirmPassword, setConfirmPassword] = useState('') 30 const [isLoading, setIsLoading] = useState(false) 31 const [error, setError] = useState('') 32 const [done, setDone] = useState(false) 33 const [notice, setNotice] = useState('') 34 const [isCaptchaCompleted, setIsCaptchaCompleted] = useState( 35 process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true, 36 ) 37 38 const handleSubmit = async (e: React.FormEvent) => { 39 e.preventDefault() 40 if (process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && !isCaptchaCompleted) 41 return setError('Please complete the CAPTCHA verification.') 42 setIsLoading(true) 43 setError('') 44 setNotice('') 45 const res = await authClient.requestPasswordReset({ 46 email, 47 redirectTo: '/reset-password', 48 } as never) 49 if (!res.error) { 50 setDone(true) 51 setNotice('Reset email sent. Please check your inbox.') 52 setIsLoading(false) 53 return 54 } 55 const fallbackBody = JSON.stringify({ 56 email, 57 redirectTo: '/reset-password', 58 }) 59 const fallbackEndpoints = [ 60 '/api/auth/forget-password', 61 '/api/auth/forgot-password', 62 '/api/auth/request-password-reset', 63 ] 64 let fallbackSucceeded = false 65 for (const endpoint of fallbackEndpoints) { 66 try { 67 const fallback = await fetch(endpoint, { 68 method: 'POST', 69 headers: { 'content-type': 'application/json' }, 70 body: fallbackBody, 71 }) 72 if (fallback.ok) { 73 fallbackSucceeded = true 74 break 75 } 76 } catch {} 77 } 78 if (fallbackSucceeded) { 79 setDone(true) 80 setNotice('Reset email sent. Please check your inbox.') 81 } else setError(res.error.message || 'An error occurred. Please try again.') 82 setIsLoading(false) 83 } 84 85 const handleResetPassword = async (e: React.FormEvent) => { 86 e.preventDefault() 87 if (process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && !isCaptchaCompleted) 88 return setError('Please complete the CAPTCHA verification.') 89 if (password !== confirmPassword) return setError('Passwords do not match.') 90 setIsLoading(true) 91 setError('') 92 const { error } = await authClient.resetPassword({ 93 newPassword: password, 94 token, 95 } as never) 96 if (error) 97 setError(error.message || 'Failed to reset password. Please try again.') 98 else setDone(true) 99 setIsLoading(false) 100 } 101 102 const isTokenFlow = Boolean(token) 103 104 return ( 105 <div className={cn('flex flex-col gap-6', className)} {...props}> 106 <Card> 107 <CardHeader className="text-center"> 108 <CardTitle className="text-xl">Reset your password</CardTitle> 109 <CardDescription> 110 {isTokenFlow 111 ? done 112 ? 'Password updated successfully. You can sign in now.' 113 : 'Enter your new password to complete reset.' 114 : done 115 ? 'Reset email sent. Please check your inbox.' 116 : "Enter your email and we'll send a reset link"} 117 </CardDescription> 118 </CardHeader> 119 <CardContent> 120 <form onSubmit={isTokenFlow ? handleResetPassword : handleSubmit}> 121 <div className="grid gap-6"> 122 {isTokenFlow ? ( 123 <> 124 <div className="grid gap-2"> 125 <Label htmlFor="password">New password</Label> 126 <Input 127 id="password" 128 type="password" 129 required 130 value={password} 131 onChange={(e) => setPassword(e.target.value)} 132 /> 133 </div> 134 <div className="grid gap-2"> 135 <Label htmlFor="confirmPassword">Confirm password</Label> 136 <Input 137 id="confirmPassword" 138 type="password" 139 required 140 value={confirmPassword} 141 onChange={(e) => setConfirmPassword(e.target.value)} 142 /> 143 </div> 144 </> 145 ) : ( 146 <div className="grid gap-2"> 147 <Label htmlFor="email">Email</Label> 148 <Input 149 id="email" 150 type="email" 151 required 152 value={email} 153 onChange={(e) => setEmail(e.target.value)} 154 /> 155 </div> 156 )} 157 {process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && ( 158 <Turnstile 159 siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY} 160 options={{ size: 'flexible' }} 161 onSuccess={() => setIsCaptchaCompleted(true)} 162 onExpire={() => setIsCaptchaCompleted(false)} 163 onError={() => { 164 setIsCaptchaCompleted(false) 165 setError('CAPTCHA initialization failed. Please try again.') 166 }} 167 /> 168 )} 169 {!isTokenFlow && notice ? ( 170 <div className="text-sm text-emerald-600">{notice}</div> 171 ) : null} 172 {error && <div className="text-sm text-red-500">{error}</div>} 173 <Button 174 type="submit" 175 className="w-full bg-[#0066ff] text-white hover:bg-[#0047cc]" 176 disabled={isLoading || !isCaptchaCompleted} 177 > 178 {isLoading 179 ? isTokenFlow 180 ? 'Updating...' 181 : 'Sending...' 182 : isTokenFlow 183 ? 'Update password' 184 : 'Send reset email'} 185 </Button> 186 </div> 187 </form> 188 </CardContent> 189 </Card> 190 191 <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 192 By clicking continue, you agree to our{' '} 193 <a href="/terms">Terms of Service</a> and{' '} 194 <a href="/privacy">Privacy Policy</a>. 195 </div> 196 </div> 197 ) 198}