-
Notifications
You must be signed in to change notification settings - Fork 285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: smtp for password reset #4630
Changes from 17 commits
874fdb2
ade34f5
e32a562
213d5f4
16bfa39
02963c3
bf7867d
6dd4021
492da61
857ee77
e77b8f7
3da3880
95def6e
d3030c1
beecfaa
4928593
04f61f8
76ecf3f
7d0615c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import React, { useCallback, useState } from "react"; | ||
import { Controller, useForm } from "react-hook-form"; | ||
import { css } from "@emotion/react"; | ||
|
||
import { Alert, Button, Form, TextField, View } from "@arizeai/components"; | ||
|
||
type InitiatePasswordResetFormParams = { | ||
email: string; | ||
}; | ||
|
||
export function ForgotPasswordForm() { | ||
const [message, setMessage] = useState<string | null>(null); | ||
const [error, setError] = useState<string | null>(null); | ||
const [isLoading, setIsLoading] = useState<boolean>(false); | ||
const onSubmit = useCallback( | ||
async (params: InitiatePasswordResetFormParams) => { | ||
setMessage(null); | ||
setError(null); | ||
setIsLoading(true); | ||
try { | ||
const response = await fetch("/auth/password-reset-email", { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify(params), | ||
}); | ||
if (!response.ok) { | ||
setError("Failed attempt"); | ||
return; | ||
} | ||
} catch (error) { | ||
setError("Failed attempt"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. needs a better error message |
||
return; | ||
} finally { | ||
setIsLoading(() => false); | ||
} | ||
setMessage( | ||
"A link to reset your password has been sent. Check your email for details." | ||
); | ||
Comment on lines
+38
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can do the follow-up but this typically swaps out the UI |
||
}, | ||
[setMessage, setError] | ||
); | ||
const { control, handleSubmit } = useForm<InitiatePasswordResetFormParams>({ | ||
RogerHYang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
defaultValues: { email: "" }, | ||
}); | ||
return ( | ||
<> | ||
{message ? ( | ||
<View paddingBottom="size-100"> | ||
<Alert variant="success">{message}</Alert> | ||
</View> | ||
) : null} | ||
{error ? ( | ||
<View paddingBottom="size-100"> | ||
<Alert variant="danger">{error}</Alert> | ||
</View> | ||
) : null} | ||
<Form> | ||
<Controller | ||
name="email" | ||
control={control} | ||
render={({ field: { onChange, value } }) => ( | ||
<TextField | ||
label="Email" | ||
name="email" | ||
isRequired | ||
type="email" | ||
onChange={onChange} | ||
value={value} | ||
placeholder="your email address" | ||
/> | ||
)} | ||
/> | ||
<div | ||
css={css` | ||
margin-top: var(--ac-global-dimension-size-400); | ||
margin-bottom: var(--ac-global-dimension-size-50); | ||
button { | ||
width: 100%; | ||
} | ||
`} | ||
> | ||
<Button | ||
variant="primary" | ||
loading={isLoading} | ||
onClick={handleSubmit(onSubmit)} | ||
> | ||
Submit | ||
</Button> | ||
</div> | ||
</Form> | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import React from "react"; | ||
|
||
import { Flex, View } from "@arizeai/components"; | ||
|
||
import { AuthLayout } from "./AuthLayout"; | ||
import { ForgotPasswordForm } from "./ForgotPasswordForm"; | ||
import { PhoenixLogo } from "./PhoenixLogo"; | ||
|
||
export function ForgotPasswordPage() { | ||
return ( | ||
<AuthLayout> | ||
<Flex direction="column" gap="size-200" alignItems="center"> | ||
<View paddingBottom="size-200"> | ||
<PhoenixLogo /> | ||
</View> | ||
</Flex> | ||
<ForgotPasswordForm /> | ||
</AuthLayout> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import React, { useCallback, useState } from "react"; | ||
import { Controller, useForm } from "react-hook-form"; | ||
import { useNavigate } from "react-router"; | ||
|
||
import { | ||
Alert, | ||
Button, | ||
Flex, | ||
Form, | ||
TextField, | ||
View, | ||
} from "@arizeai/components"; | ||
|
||
const MIN_PASSWORD_LENGTH = 4; | ||
|
||
export type ResetPasswordWithTokenFormParams = { | ||
resetToken: string; | ||
newPassword: string; | ||
confirmPassword: string; | ||
}; | ||
|
||
interface ResetPasswordWithTokenFormProps { | ||
resetToken: string; | ||
} | ||
|
||
export function ResetPasswordWithTokenForm({ | ||
resetToken, | ||
}: ResetPasswordWithTokenFormProps) { | ||
const navigate = useNavigate(); | ||
const [message, setMessage] = useState<string | null>(null); | ||
const [error, setError] = useState<string | null>(null); | ||
const [isLoading, setIsLoading] = useState<boolean>(false); | ||
const onSubmit = useCallback( | ||
async ({ resetToken, newPassword }: ResetPasswordWithTokenFormParams) => { | ||
setMessage(null); | ||
setError(null); | ||
setIsLoading(true); | ||
try { | ||
const response = await fetch("/auth/password-reset", { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ token: resetToken, password: newPassword }), | ||
}); | ||
if (!response.ok) { | ||
setError("Failed attempt"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not helpful error message |
||
return; | ||
} | ||
} catch (error) { | ||
setError("Failed attempt"); | ||
return; | ||
} finally { | ||
setIsLoading(() => false); | ||
} | ||
setMessage("Success"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not helpful message There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
navigate("/login"); | ||
}, | ||
[setMessage, setError, navigate] | ||
); | ||
const { | ||
control, | ||
handleSubmit, | ||
formState: { isDirty }, | ||
} = useForm<ResetPasswordWithTokenFormParams>({ | ||
defaultValues: { | ||
resetToken: resetToken, | ||
newPassword: "", | ||
confirmPassword: "", | ||
}, | ||
}); | ||
return ( | ||
<> | ||
{message ? ( | ||
<View paddingBottom="size-100"> | ||
<Alert variant="success">{message}</Alert> | ||
</View> | ||
) : null} | ||
{error ? ( | ||
<View paddingBottom="size-100"> | ||
<Alert variant="danger">{error}</Alert> | ||
</View> | ||
) : null} | ||
<Form onSubmit={handleSubmit(onSubmit)}> | ||
<Controller | ||
name="newPassword" | ||
control={control} | ||
rules={{ | ||
required: "Password is required", | ||
minLength: { | ||
value: MIN_PASSWORD_LENGTH, | ||
message: `Password must be at least ${MIN_PASSWORD_LENGTH} characters`, | ||
}, | ||
}} | ||
render={({ | ||
field: { name, onChange, onBlur, value }, | ||
fieldState: { invalid, error }, | ||
}) => ( | ||
<TextField | ||
label="New Password" | ||
type="password" | ||
isRequired | ||
description={`Password must be at least ${MIN_PASSWORD_LENGTH} characters`} | ||
name={name} | ||
errorMessage={error?.message} | ||
validationState={invalid ? "invalid" : "valid"} | ||
onChange={onChange} | ||
onBlur={onBlur} | ||
defaultValue={value} | ||
/> | ||
)} | ||
/> | ||
<Controller | ||
name="confirmPassword" | ||
control={control} | ||
rules={{ | ||
required: "Password is required", | ||
minLength: { | ||
value: MIN_PASSWORD_LENGTH, | ||
message: `Password must be at least ${MIN_PASSWORD_LENGTH} characters`, | ||
}, | ||
validate: (value, formValues) => | ||
value === formValues.newPassword || "Passwords do not match", | ||
}} | ||
render={({ | ||
field: { name, onChange, onBlur, value }, | ||
fieldState: { invalid, error }, | ||
}) => ( | ||
<TextField | ||
label="Confirm Password" | ||
isRequired | ||
type="password" | ||
description="Confirm the new password" | ||
name={name} | ||
errorMessage={error?.message} | ||
validationState={invalid ? "invalid" : "valid"} | ||
onChange={onChange} | ||
onBlur={onBlur} | ||
defaultValue={value} | ||
/> | ||
)} | ||
/> | ||
<View paddingTop="size-200"> | ||
<Flex direction="row" gap="size-100" justifyContent="end"> | ||
<Button | ||
variant={isDirty ? "primary" : "default"} | ||
type="submit" | ||
disabled={isLoading} | ||
> | ||
{isLoading ? "Resetting..." : "Reset Password"} | ||
</Button> | ||
</Flex> | ||
</View> | ||
</Form> | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import React from "react"; | ||
import { useNavigate } from "react-router"; | ||
import { useSearchParams } from "react-router-dom"; | ||
|
||
import { Flex, View } from "@arizeai/components"; | ||
|
||
import { AuthLayout } from "./AuthLayout"; | ||
import { PhoenixLogo } from "./PhoenixLogo"; | ||
import { ResetPasswordWithTokenForm } from "./ResetPasswordWithTokenForm"; | ||
|
||
export function ResetPasswordWithTokenPage() { | ||
const navigate = useNavigate(); | ||
const [searchParams] = useSearchParams(); | ||
const token = searchParams.get("token"); | ||
if (!token) { | ||
navigate("/login"); | ||
return null; | ||
} | ||
Comment on lines
+14
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would probably just show an error. We need a "back to login" link anyways. Redirecting typically should happen on user-action - it might be confusing otherwise. |
||
return ( | ||
<AuthLayout> | ||
<Flex direction="column" gap="size-200" alignItems="center"> | ||
<View paddingBottom="size-200"> | ||
<PhoenixLogo /> | ||
</View> | ||
</Flex> | ||
<ResetPasswordWithTokenForm resetToken={token} /> | ||
</AuthLayout> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
export * from "./LoginPage"; | ||
export * from "./ResetPasswordPage"; | ||
export * from "./ResetPasswordWithTokenPage"; | ||
export * from "./resetPasswordLoader"; | ||
export * from "./ForgotPasswordPage"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs a better error message