| 
									
										
										
										
											2021-06-24 15:59:11 +00:00
										 |  |  | import { getCsrfToken } from "next-auth/client"; | 
					
						
							|  |  |  | import prisma from "../../../lib/prisma"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import Head from "next/head"; | 
					
						
							|  |  |  | import React from "react"; | 
					
						
							|  |  |  | import debounce from "lodash.debounce"; | 
					
						
							|  |  |  | import dayjs from "dayjs"; | 
					
						
							|  |  |  | import { ResetPasswordRequest } from "@prisma/client"; | 
					
						
							|  |  |  | import { useMemo } from "react"; | 
					
						
							|  |  |  | import Link from "next/link"; | 
					
						
							|  |  |  | import { GetServerSidePropsContext } from "next"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type Props = { | 
					
						
							|  |  |  |   id: string; | 
					
						
							|  |  |  |   resetPasswordRequest: ResetPasswordRequest; | 
					
						
							|  |  |  |   csrfToken: string; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default function Page({ resetPasswordRequest, csrfToken }: Props) { | 
					
						
							|  |  |  |   const [loading, setLoading] = React.useState(false); | 
					
						
							|  |  |  |   const [error, setError] = React.useState(null); | 
					
						
							|  |  |  |   const [success, setSuccess] = React.useState(false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const [password, setPassword] = React.useState(""); | 
					
						
							|  |  |  |   const handleChange = (e) => { | 
					
						
							|  |  |  |     setPassword(e.target.value); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const submitChangePassword = async ({ password, requestId }) => { | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       const res = await fetch("/api/auth/reset-password", { | 
					
						
							|  |  |  |         method: "POST", | 
					
						
							|  |  |  |         body: JSON.stringify({ requestId: requestId, password: password }), | 
					
						
							|  |  |  |         headers: { | 
					
						
							|  |  |  |           "Content-Type": "application/json", | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const json = await res.json(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (!res.ok) { | 
					
						
							|  |  |  |         setError(json); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         setSuccess(true); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return json; | 
					
						
							|  |  |  |     } catch (reason) { | 
					
						
							|  |  |  |       setError({ message: "An unexpected error occurred. Try again." }); | 
					
						
							|  |  |  |     } finally { | 
					
						
							|  |  |  |       setLoading(false); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const debouncedChangePassword = debounce(submitChangePassword, 250); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const handleSubmit = async (e) => { | 
					
						
							|  |  |  |     e.preventDefault(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!password) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (loading) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setLoading(true); | 
					
						
							|  |  |  |     setError(null); | 
					
						
							|  |  |  |     setSuccess(false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     await debouncedChangePassword({ password, requestId: resetPasswordRequest.id }); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const Success = () => { | 
					
						
							|  |  |  |     return ( | 
					
						
							|  |  |  |       <> | 
					
						
							|  |  |  |         <div className="space-y-6"> | 
					
						
							|  |  |  |           <div> | 
					
						
							|  |  |  |             <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Success</h2> | 
					
						
							|  |  |  |           </div> | 
					
						
							|  |  |  |           <p>Your password has been reset. You can now login with your newly created password.</p> | 
					
						
							|  |  |  |           <Link href="/auth/login"> | 
					
						
							|  |  |  |             <button | 
					
						
							|  |  |  |               type="button" | 
					
						
							| 
									
										
										
										
											2021-08-02 15:24:01 +00:00
										 |  |  |               className="w-full flex justify-center py-2 px-4 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"> | 
					
						
							| 
									
										
										
										
											2021-06-24 15:59:11 +00:00
										 |  |  |               Login | 
					
						
							|  |  |  |             </button> | 
					
						
							|  |  |  |           </Link> | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |       </> | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const Expired = () => { | 
					
						
							|  |  |  |     return ( | 
					
						
							|  |  |  |       <> | 
					
						
							|  |  |  |         <div className="space-y-6"> | 
					
						
							|  |  |  |           <div> | 
					
						
							|  |  |  |             <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Whoops</h2> | 
					
						
							|  |  |  |             <h2 className="text-center text-3xl font-extrabold text-gray-900">That Request is Expired.</h2> | 
					
						
							|  |  |  |           </div> | 
					
						
							|  |  |  |           <p> | 
					
						
							|  |  |  |             That request is expired. You can back and enter the email associated with your account and we will | 
					
						
							|  |  |  |             you another link to reset your password. | 
					
						
							|  |  |  |           </p> | 
					
						
							|  |  |  |           <Link href="/auth/forgot-password"> | 
					
						
							|  |  |  |             <button | 
					
						
							|  |  |  |               type="button" | 
					
						
							| 
									
										
										
										
											2021-08-02 15:24:01 +00:00
										 |  |  |               className="w-full flex justify-center py-2 px-4 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"> | 
					
						
							| 
									
										
										
										
											2021-06-24 15:59:11 +00:00
										 |  |  |               Try Again | 
					
						
							|  |  |  |             </button> | 
					
						
							|  |  |  |           </Link> | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |       </> | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const isRequestExpired = useMemo(() => { | 
					
						
							|  |  |  |     const now = dayjs(); | 
					
						
							|  |  |  |     return dayjs(resetPasswordRequest.expires).isBefore(now); | 
					
						
							|  |  |  |   }, [resetPasswordRequest]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     <div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8"> | 
					
						
							|  |  |  |       <Head> | 
					
						
							|  |  |  |         <title>Reset Password</title> | 
					
						
							|  |  |  |         <link rel="icon" href="/favicon.ico" /> | 
					
						
							|  |  |  |       </Head> | 
					
						
							|  |  |  |       <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> | 
					
						
							|  |  |  |         <div className="bg-white py-8 px-4 mx-2 shadow rounded-lg sm:px-10 space-y-6"> | 
					
						
							|  |  |  |           {isRequestExpired && <Expired />} | 
					
						
							|  |  |  |           {!isRequestExpired && !success && ( | 
					
						
							|  |  |  |             <> | 
					
						
							|  |  |  |               <div className="space-y-6"> | 
					
						
							|  |  |  |                 <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Reset Password</h2> | 
					
						
							|  |  |  |                 <p>Enter the new password you'd like for your account.</p> | 
					
						
							|  |  |  |                 {error && <p className="text-red-600">{error.message}</p>} | 
					
						
							|  |  |  |               </div> | 
					
						
							|  |  |  |               <form className="space-y-6" onSubmit={handleSubmit} action="#"> | 
					
						
							|  |  |  |                 <input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden /> | 
					
						
							|  |  |  |                 <div> | 
					
						
							|  |  |  |                   <label htmlFor="email" className="block text-sm font-medium text-gray-700"> | 
					
						
							|  |  |  |                     New Password | 
					
						
							|  |  |  |                   </label> | 
					
						
							|  |  |  |                   <div className="mt-1"> | 
					
						
							|  |  |  |                     <input | 
					
						
							|  |  |  |                       onChange={handleChange} | 
					
						
							|  |  |  |                       id="password" | 
					
						
							|  |  |  |                       name="password" | 
					
						
							|  |  |  |                       type="password" | 
					
						
							|  |  |  |                       autoComplete="password" | 
					
						
							|  |  |  |                       required | 
					
						
							| 
									
										
										
										
											2021-08-02 15:24:01 +00:00
										 |  |  |                       className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-black focus:border-black sm:text-sm" | 
					
						
							| 
									
										
										
										
											2021-06-24 15:59:11 +00:00
										 |  |  |                     /> | 
					
						
							|  |  |  |                   </div> | 
					
						
							|  |  |  |                 </div> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 <div> | 
					
						
							|  |  |  |                   <button | 
					
						
							|  |  |  |                     type="submit" | 
					
						
							|  |  |  |                     disabled={loading} | 
					
						
							| 
									
										
										
										
											2021-08-02 15:24:01 +00:00
										 |  |  |                     className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black ${ | 
					
						
							| 
									
										
										
										
											2021-06-24 15:59:11 +00:00
										 |  |  |                       loading ? "cursor-not-allowed" : "" | 
					
						
							|  |  |  |                     }`}>
 | 
					
						
							|  |  |  |                     {loading && ( | 
					
						
							|  |  |  |                       <svg | 
					
						
							|  |  |  |                         className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" | 
					
						
							|  |  |  |                         xmlns="http://www.w3.org/2000/svg" | 
					
						
							|  |  |  |                         fill="none" | 
					
						
							|  |  |  |                         viewBox="0 0 24 24"> | 
					
						
							|  |  |  |                         <circle | 
					
						
							|  |  |  |                           className="opacity-25" | 
					
						
							|  |  |  |                           cx="12" | 
					
						
							|  |  |  |                           cy="12" | 
					
						
							|  |  |  |                           r="10" | 
					
						
							|  |  |  |                           stroke="currentColor" | 
					
						
							|  |  |  |                           strokeWidth="4"></circle> | 
					
						
							|  |  |  |                         <path | 
					
						
							|  |  |  |                           className="opacity-75" | 
					
						
							|  |  |  |                           fill="currentColor" | 
					
						
							|  |  |  |                           d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | 
					
						
							|  |  |  |                       </svg> | 
					
						
							|  |  |  |                     )} | 
					
						
							|  |  |  |                     Submit | 
					
						
							|  |  |  |                   </button> | 
					
						
							|  |  |  |                 </div> | 
					
						
							|  |  |  |               </form> | 
					
						
							|  |  |  |             </> | 
					
						
							|  |  |  |           )} | 
					
						
							|  |  |  |           {!isRequestExpired && success && ( | 
					
						
							|  |  |  |             <> | 
					
						
							|  |  |  |               <Success /> | 
					
						
							|  |  |  |             </> | 
					
						
							|  |  |  |           )} | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |       </div> | 
					
						
							|  |  |  |     </div> | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export async function getServerSideProps(context: GetServerSidePropsContext) { | 
					
						
							|  |  |  |   const id = context.params.id; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   try { | 
					
						
							|  |  |  |     const resetPasswordRequest = await prisma.resetPasswordRequest.findUnique({ | 
					
						
							|  |  |  |       where: { | 
					
						
							|  |  |  |         id: id, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       select: { | 
					
						
							|  |  |  |         id: true, | 
					
						
							|  |  |  |         expires: true, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       props: { | 
					
						
							|  |  |  |         resetPasswordRequest: { | 
					
						
							|  |  |  |           ...resetPasswordRequest, | 
					
						
							|  |  |  |           expires: resetPasswordRequest.expires.toString(), | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         id, | 
					
						
							|  |  |  |         csrfToken: await getCsrfToken({ req: context.req }), | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } catch (reason) { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       notFound: true, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |