11"use client" ;
22
33import Link from "next/link" ;
4- import type { CSSProperties } from "react" ;
4+ import { useEffect , useReducer , type CSSProperties } from "react" ;
5+ import type { UseFormReturn } from "react-hook-form" ;
56import { useForm } from "react-hook-form" ;
67
78import { useLocale } from "@calcom/lib/hooks/useLocale" ;
@@ -16,121 +17,176 @@ import type { getServerSideProps } from "@server/lib/auth/forgot-password/[id]/g
1617
1718export type PageProps = inferSSRProps < typeof getServerSideProps > ;
1819
19- export default function Page ( { requestId , isRequestExpired , csrfToken } : PageProps ) {
20+ function Success ( ) {
2021 const { t } = useLocale ( ) ;
21- const formMethods = useForm < { new_password : string } > ( ) ;
22- const success = formMethods . formState . isSubmitSuccessful ;
23- const loading = formMethods . formState . isSubmitting ;
24- const passwordValue = formMethods . watch ( "new_password" ) ;
25- const isEmpty = passwordValue ?. length === 0 ;
22+ return (
23+ < >
24+ < div className = "space-y-6" >
25+ < div >
26+ < h2 className = "font-cal text-emphasis mt-6 text-center text-3xl font-extrabold" >
27+ { t ( "password_updated" ) }
28+ </ h2 >
29+ </ div >
30+ < Button href = "/auth/login" className = "w-full justify-center" >
31+ { t ( "login" ) }
32+ </ Button >
33+ </ div >
34+ </ >
35+ ) ;
36+ }
37+
38+ function Expired ( ) {
39+ const { t } = useLocale ( ) ;
40+ return (
41+ < >
42+ < div className = "space-y-6" >
43+ < div >
44+ < h2 className = "font-cal text-emphasis mt-6 text-center text-3xl font-extrabold" > { t ( "whoops" ) } </ h2 >
45+ < h2 className = "text-emphasis text-center text-3xl font-extrabold" > { t ( "request_is_expired" ) } </ h2 >
46+ </ div >
47+ < p > { t ( "request_is_expired_instructions" ) } </ p >
48+ < Link href = "/auth/forgot-password" passHref legacyBehavior >
49+ < button
50+ type = "button"
51+ className = "flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2" >
52+ { t ( "try_again" ) }
53+ </ button >
54+ </ Link >
55+ </ div >
56+ </ >
57+ ) ;
58+ }
59+
60+ type FormValues = {
61+ newPassword : string ;
62+ csrfToken : string ;
63+ } ;
2664
27- const submitChangePassword = async ( { password, requestId } : { password : string ; requestId : string } ) => {
65+ function PasswordResetForm ( {
66+ form : formMethods ,
67+ requestId,
68+ } : {
69+ form : UseFormReturn < FormValues > ;
70+ requestId : string ;
71+ } ) {
72+ const { t } = useLocale ( ) ;
73+ const [ refreshToken , forceRefresh ] = useReducer ( ( x ) => x + 1 , 0 ) ;
74+ const {
75+ watch,
76+ setValue,
77+ setError,
78+ formState : { isSubmitting : loading } ,
79+ } = formMethods ;
80+
81+ useEffect ( ( ) => {
82+ fetch ( "/api/csrf" , { cache : "no-store" } )
83+ . then ( ( res ) => res . json ( ) )
84+ . then ( ( { csrfToken } ) => setValue ( "csrfToken" , csrfToken ) )
85+ . catch ( ( ) => setValue ( "csrfToken" , "" ) ) ;
86+ } , [ setValue , refreshToken ] ) ;
87+
88+ const submitChangePassword = async ( {
89+ password,
90+ requestId,
91+ csrfToken,
92+ } : {
93+ password : string ;
94+ requestId : string ;
95+ csrfToken : string ;
96+ } ) => {
2897 const res = await fetch ( "/api/auth/reset-password" , {
2998 method : "POST" ,
30- body : JSON . stringify ( { requestId, password } ) ,
99+ body : JSON . stringify ( { requestId, password, csrfToken } ) ,
31100 headers : {
32101 "Content-Type" : "application/json" ,
33102 } ,
34103 } ) ;
35104 const json = await res . json ( ) ;
36- if ( ! res . ok ) return formMethods . setError ( "new_password" , { type : "server" , message : json . message } ) ;
105+ if ( ! res . ok ) {
106+ // if the request fails, we want to force refresh of the CSRF token - this allows resubmit
107+ forceRefresh ( ) ;
108+ return setError ( "newPassword" , { type : "server" , message : json . message } ) ;
109+ }
37110 } ;
38111
39- const Success = ( ) => {
40- return (
41- < >
42- < div className = "space-y-6" >
43- < div >
44- < h2 className = "font-cal text-emphasis mt-6 text-center text-3xl font-extrabold" >
45- { t ( "password_updated" ) }
46- </ h2 >
47- </ div >
48- < Button href = "/auth/login" className = "w-full justify-center" >
49- { t ( "login" ) }
50- </ Button >
51- </ div >
52- </ >
53- ) ;
54- } ;
112+ const passwordValue = watch ( "newPassword" ) ;
113+ const isEmpty = passwordValue ?. length === 0 ;
55114
56- const Expired = ( ) => {
115+ return (
116+ < Form
117+ className = "space-y-6"
118+ form = { formMethods }
119+ style = {
120+ {
121+ "--cal-brand" : "#111827" ,
122+ "--cal-brand-emphasis" : "#101010" ,
123+ "--cal-brand-text" : "white" ,
124+ "--cal-brand-subtle" : "#9CA3AF" ,
125+ } as CSSProperties
126+ }
127+ handleSubmit = { async ( values ) => {
128+ await submitChangePassword ( {
129+ password : values . newPassword ,
130+ csrfToken : values . csrfToken ,
131+ requestId,
132+ } ) ;
133+ } } >
134+ < input { ...formMethods . register ( "csrfToken" ) } name = "csrfToken" type = "hidden" hidden />
135+ < div className = "mt-1" >
136+ < PasswordField
137+ { ...formMethods . register ( "newPassword" , {
138+ minLength : {
139+ message : t ( "password_hint_min" ) ,
140+ value : 7 , // We don't have user here so we can't check if they are admin or not
141+ } ,
142+ pattern : {
143+ message : "Should contain a number, uppercase and lowercase letters" ,
144+ value : / ^ (? = .* \d ) (? = .* [ a - z ] ) (? = .* [ A - Z ] ) (? = .* [ a - z A - Z ] ) .* $ / gm,
145+ } ,
146+ } ) }
147+ label = { t ( "new_password" ) }
148+ />
149+ </ div >
150+
151+ < div >
152+ < Button
153+ loading = { loading }
154+ color = "primary"
155+ type = "submit"
156+ disabled = { loading || isEmpty }
157+ className = "w-full justify-center" >
158+ { t ( "reset_password" ) }
159+ </ Button >
160+ </ div >
161+ </ Form >
162+ ) ;
163+ }
164+
165+ export default function Page ( { requestId, isRequestExpired } : PageProps ) {
166+ const { t } = useLocale ( ) ;
167+
168+ const formMethods = useForm < FormValues > ( {
169+ defaultValues : {
170+ newPassword : "" ,
171+ csrfToken : "" ,
172+ } ,
173+ } ) ;
174+
175+ const {
176+ formState : { isSubmitSuccessful : success } ,
177+ } = formMethods ;
178+
179+ if ( isRequestExpired ) {
57180 return (
58- < >
59- < div className = "space-y-6" >
60- < div >
61- < h2 className = "font-cal text-emphasis mt-6 text-center text-3xl font-extrabold" > { t ( "whoops" ) } </ h2 >
62- < h2 className = "text-emphasis text-center text-3xl font-extrabold" > { t ( "request_is_expired" ) } </ h2 >
63- </ div >
64- < p > { t ( "request_is_expired_instructions" ) } </ p >
65- < Link href = "/auth/forgot-password" passHref legacyBehavior >
66- < button
67- type = "button"
68- className = "flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2" >
69- { t ( "try_again" ) }
70- </ button >
71- </ Link >
72- </ div >
73- </ >
181+ < AuthContainer showLogo heading = { t ( "reset_password" ) } >
182+ < Expired />
183+ </ AuthContainer >
74184 ) ;
75- } ;
185+ }
76186
77187 return (
78188 < AuthContainer showLogo heading = { ! success ? t ( "reset_password" ) : undefined } >
79- { isRequestExpired && < Expired /> }
80- { ! isRequestExpired && ! success && (
81- < >
82- < Form
83- className = "space-y-6"
84- form = { formMethods }
85- style = {
86- {
87- "--cal-brand" : "#111827" ,
88- "--cal-brand-emphasis" : "#101010" ,
89- "--cal-brand-text" : "white" ,
90- "--cal-brand-subtle" : "#9CA3AF" ,
91- } as CSSProperties
92- }
93- handleSubmit = { async ( values ) => {
94- await submitChangePassword ( {
95- password : values . new_password ,
96- requestId,
97- } ) ;
98- } } >
99- < input name = "csrfToken" type = "hidden" defaultValue = { csrfToken } hidden />
100- < div className = "mt-1" >
101- < PasswordField
102- { ...formMethods . register ( "new_password" , {
103- minLength : {
104- message : t ( "password_hint_min" ) ,
105- value : 7 , // We don't have user here so we can't check if they are admin or not
106- } ,
107- pattern : {
108- message : "Should contain a number, uppercase and lowercase letters" ,
109- value : / ^ (? = .* \d ) (? = .* [ a - z ] ) (? = .* [ A - Z ] ) (? = .* [ a - z A - Z ] ) .* $ / gm,
110- } ,
111- } ) }
112- label = { t ( "new_password" ) }
113- />
114- </ div >
115-
116- < div >
117- < Button
118- loading = { loading }
119- color = "primary"
120- type = "submit"
121- disabled = { loading || isEmpty }
122- className = "w-full justify-center" >
123- { t ( "reset_password" ) }
124- </ Button >
125- </ div >
126- </ Form >
127- </ >
128- ) }
129- { ! isRequestExpired && success && (
130- < >
131- < Success />
132- </ >
133- ) }
189+ { success ? < Success /> : < PasswordResetForm form = { formMethods } requestId = { requestId } /> }
134190 </ AuthContainer >
135191 ) ;
136192}
0 commit comments