Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
austensen committed Mar 21, 2024
1 parent 73e021d commit 7c0a5b4
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 86 deletions.
38 changes: 38 additions & 0 deletions client/src/components/AuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ type VerifyEmailResponse = {
error?: string;
};

export enum ResetStatusCode {
Success = 200,
Accepted = 202,
Invalid = 400,
Expired = 404,
Unknown = 500,
}

type PasswordResetResponse = Omit<VerifyEmailResponse, "statusCode"> & {
statusCode: ResetStatusCode;
};

let _user: JustfixUser | undefined;
const user = () => _user;
const fetchUser = async () => {
Expand Down Expand Up @@ -77,6 +89,31 @@ const resetPasswordRequest = async (username: string) => {
return await postAuthRequest(`${BASE_URL}auth/reset_password`, { username });
};

/**
* Sends an unauthenticated request to checks if the password reset token is valid
*/
const resetPasswordCheck = async () => {
const params = new URLSearchParams(window.location.search);

let result: PasswordResetResponse = {
statusCode: ResetStatusCode.Unknown,
statusText: "",
};

try {
const response = await postAuthRequest(
`${BASE_URL}auth/reset_password/check?token=${params.get("token")}`
);
result.statusCode = response.status_code;
result.statusText = response.status_text;
} catch (e) {
if (e instanceof Error) {
result.error = e.message;
}
}
return result;
};

const resetPassword = async (token: string, newPassword: string) => {
return await postAuthRequest(`${BASE_URL}auth/set_password?token=${token}`, {
new_password: newPassword,
Expand Down Expand Up @@ -302,6 +339,7 @@ const Client = {
updateEmail,
updatePassword,
resetPasswordRequest,
resetPasswordCheck,
resetPassword,
buildingSubscribe,
buildingUnsubscribe,
Expand Down
69 changes: 40 additions & 29 deletions client/src/containers/ForgotPasswordPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,37 @@ import { withI18n, withI18nProps } from "@lingui/react";
import { Trans, t } from "@lingui/macro";

import { UserContext } from "components/UserContext";
import { MailIcon } from "components/Icons";
import EmailInput from "components/EmailInput";
import { useInput } from "util/helpers";

const ForgotPasswordPage = withI18n()((props: withI18nProps) => {
const { i18n } = props;
const { search } = useLocation();
const params = new URLSearchParams(search);

const [requestSent, setRequestSent] = React.useState(false);
const [email, setEmail] = React.useState(decodeURIComponent(params.get("email") || ""));
const userContext = useContext(UserContext);
const {
value: email,
error: emailError,
showError: showEmailError,
setError: setEmailError,
setShowError: setShowEmailError,
onChange: onChangeEmail,
} = useInput(decodeURIComponent(params.get("email") || ""));

const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!email || emailError) {
setEmailError(true);
setShowEmailError(true);
return;
}
// TODO: should we confirm that the email already exists?
resendPasswordResetRequest();
};

const resendPasswordResetRequest = async () => {
await userContext.requestPasswordReset(email);
setRequestSent(true);
};
Expand All @@ -35,43 +48,41 @@ const ForgotPasswordPage = withI18n()((props: withI18nProps) => {
<div className="ForgotPasswordPage Page">
<div className="page-container">
<Trans render="h4" className="page-title">
Forgot your password?
Reset Password
</Trans>
{!requestSent ? (
<>
<Trans render="h5">
Review your email address below. You’ll receive a "Reset password" email to this
address.
</Trans>
<form onSubmit={handleSubmit}>
<Trans render="label">Email address</Trans>
<input
type="email"
className="input"
<form onSubmit={handleSubmit} className="input-group">
<EmailInput
email={email}
error={emailError}
showError={showEmailError}
setError={setEmailError}
onChange={onChangeEmail}
placeholder={i18n._(t`Enter email`)}
onChange={handleValueChange}
value={email}
/>
<input
type="submit"
className="button is-primary"
value={i18n._(t`Reset password`)}
labelText={i18n._(t`Email address`)}
autoFocus
/>
<button type="submit" className="button is-primary">
<Trans>Reset password</Trans>
</button>
</form>
</>
) : (
<div className="request-sent-success">
<MailIcon />
<Trans render="h5">
An email has been sent to your email address {`${email}`}. Please check your inbox
and spam.
We sent a reset link to {`${email}`}. Please check your inbox and spam.
</Trans>
<button className="button is-text" onClick={() => setRequestSent(false)}>
<Trans>
Didn’t receive an email?
<br />
Click here to try again.
</Trans>
<Trans render="span">Didn’t receive an email?</Trans>
<button
className="button is-primary resend-link"
onClick={resendPasswordResetRequest}
>
<Trans>Send new link</Trans>
</button>
</div>
)}
Expand Down
152 changes: 102 additions & 50 deletions client/src/containers/ResetPasswordPage.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import React, { useContext } from "react";
import React, { useEffect } from "react";
import { useLocation } from "react-router-dom";
import LegalFooter from "../components/LegalFooter";

import Page from "../components/Page";
import { withI18n, withI18nProps } from "@lingui/react";
import { Trans, t } from "@lingui/macro";

import { UserContext } from "components/UserContext";
import AuthClient, { ResetStatusCode } from "components/AuthClient";
import PasswordInput from "components/PasswordInput";
import { useInput } from "util/helpers";
import { FixedLoadingLabel } from "components/Loader";

const ResetPasswordPage = withI18n()((props: withI18nProps) => {
const { i18n } = props;
const { search } = useLocation();
const params = new URLSearchParams(search);

const [tokenStatus, setTokenStatus] = React.useState<ResetStatusCode>();
const [requestSent, setRequestSent] = React.useState(false);
const userContext = useContext(UserContext);
const {
value: password,
error: passwordError,
showError: showPasswordError,
setShowError: setShowPasswordError,
setError: setPasswordError,
onChange: onChangePassword,
} = useInput("");
Expand All @@ -42,61 +44,111 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => {
}, delayInterval);
};

useEffect(() => {
const asyncCheckToken = async () => {
return await AuthClient.resetPasswordCheck();
};

asyncCheckToken().then((result) => {
setTokenStatus(result.statusCode);
});
}, []);

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

await userContext.resetPassword(params.get("token") || "", password);
setRequestSent(true);
if (!password || passwordError) {
setPasswordError(true);
setShowPasswordError(true);
return;
}

const resp = await AuthClient.resetPassword(params.get("token") || "", password);
if (resp.statusCode === ResetStatusCode.Success) {

} else {
setTokenStatus(resp.statusCode);
}
};

const expiredTokenPage = () => (
<>
<Trans render="h4">The link sent to you has timed out.</Trans>
<br />
<button
className="button is-secondary"
onClick={async () => {
setIsEmailResent(await AuthClient.resendVerifyEmail());
}}
>
<Trans>Resend verification email</Trans>
</button>
</>
)

const invalidTokenPage = () => (
<>
<Trans render="h4">The link sent to you has timed out.</Trans>
<br />
<button
className="button is-secondary"
onClick={async () => {
setIsEmailResent(await AuthClient.resendVerifyEmail());
}}
>
<Trans>Resend verification email</Trans>
</button>
</>
)

const resetPasswordPage = () => (
<>
<Trans render="h4">Reset your password</Trans>
<form className="input-group" onSubmit={handleSubmit}>
<PasswordInput
labelText={i18n._(t`Create a password`)}
showPasswordRules={true}
password={password}
onChange={onChangePassword}
error={passwordError}
showError={showPasswordError}
setError={setPasswordError}
/>
<button type="submit" className="button is-primary">
<Trans>Reset password</Trans>
</button>
</form>
</>
);

const successPage = () => (
<>
<Trans className="text-center" render="h4">
Your password has successfully been reset
</Trans>
<br />
<div className="text-center">
<Trans className="text-center">You will be redirected back to Who Owns What in</Trans>
<br>{updateCountdown()}</br>
<Trans className="d-flex justify-content-center">
<span id="countdown"> {delaySeconds}</span> seconds
</Trans>
<br />
<br />
<Trans className="text-center">
<a href={redirectUrl} style={{ color: "#242323" }}>
Click to log in
</a>{" "}
if you are not redirected
</Trans>
</div>
</>
);

return (
<Page title={i18n._(t`Reset your password`)}>
<div className="ResetPasswordPage Page">
<div className="page-container">
{!requestSent ? (
<>
<Trans render="h4">Reset your password</Trans>
<form className="input-group" onSubmit={handleSubmit}>
<PasswordInput
labelText={i18n._(t`Create a password`)}
showPasswordRules={true}
password={password}
onChange={onChangePassword}
error={passwordError}
showError={showPasswordError}
setError={setPasswordError}
/>
<button type="submit" className="button is-primary">
<Trans>Reset password</Trans>
</button>
</form>
</>
) : (
<>
<Trans className="text-center" render="h4">
Your password has successfully been reset
</Trans>
<br />
<div className="text-center">
<Trans className="text-center">
You will be redirected back to Who Owns What in
</Trans>
<br>{updateCountdown()}</br>
<Trans className="d-flex justify-content-center">
<span id="countdown"> {delaySeconds}</span> seconds
</Trans>
<br />
<br />
<Trans className="text-center">
<a href={redirectUrl} style={{ color: "#242323" }}>
Click to log in
</a>{" "}
if you are not redirected
</Trans>
</div>
</>
)}
</div>
<div className="page-container">{tokenIsValid === undefined ? <FixedLoadingLabel/> : !tokenIsValid ? invalidTokenPage() : passwordIsReset ? successPage() : resetPasswordPage()}</div>
<LegalFooter />
</div>
</Page>
Expand Down
16 changes: 9 additions & 7 deletions client/src/styles/ForgotPasswordPage.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
@import "_vars.scss";
@import "_typography.scss";

.ForgotPasswordPage {
.ForgotPasswordPage,
.ResetPasswordPage {
.page-container {
display: flex;
flex-direction: column;
Expand All @@ -18,6 +19,13 @@
margin: 0;
}

.button.is-primary {
margin: 0 auto;
&.resend-link {
margin-top: 1rem;
}
}

label {
@include desktop-eyebrow();

Expand All @@ -26,12 +34,6 @@
margin-bottom: 0.19rem;
}

input[type="submit"] {
padding: 0.63rem 1.25rem !important;
align-self: center;
margin: 1.02rem auto 0.23rem auto !important;
}

.request-sent-success {
display: flex;
flex-direction: column;
Expand Down
Loading

0 comments on commit 7c0a5b4

Please sign in to comment.