From b1daae95eeb15205f92030089dd4c0f1152a372a Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Sun, 12 Nov 2023 03:18:30 -0500
Subject: [PATCH 1/6] password reset forms
---
_redirects | 1 +
packages/components/src/api/ApiConfig.ts | 8 +++
.../src/api/user/RedeemResetPassword.ts | 57 ++++++++++++++++
.../src/api/user/RequestResetPassword.ts | 55 +++++++++++++++
.../src/v2/view/pages/login/LoginPage.tsx | 6 +-
.../password_reset/PasswordResetEmailPage.tsx | 67 +++++++++++++++++--
6 files changed, 185 insertions(+), 9 deletions(-)
create mode 100644 packages/components/src/api/user/RedeemResetPassword.ts
create mode 100644 packages/components/src/api/user/RequestResetPassword.ts
diff --git a/_redirects b/_redirects
index e58f547a2a..0e23aee174 100644
--- a/_redirects
+++ b/_redirects
@@ -23,6 +23,7 @@
/news* /index.html 200
/obs_configs* /index.html 200
/old* /index.html 200
+/password-reset* /index.html 200
/patrons* /index.html 200
/pending* /index.html 200
/portal_success* /index.html 200
diff --git a/packages/components/src/api/ApiConfig.ts b/packages/components/src/api/ApiConfig.ts
index 2aa8220648..34b1f4e544 100644
--- a/packages/components/src/api/ApiConfig.ts
+++ b/packages/components/src/api/ApiConfig.ts
@@ -103,6 +103,14 @@ class ApiConfig {
return `${this.getApiOrigin()}/v1/login`;
}
+ passwordResetRequest(): string {
+ return `${this.getApiOrigin()}/v1/password_reset/request`;
+ }
+
+ passwordResetRedeem(): string {
+ return `${this.getApiOrigin()}/v1/password_reset/redeem`;
+ }
+
logout(): string {
return `${this.getApiOrigin()}/v1/logout`;
}
diff --git a/packages/components/src/api/user/RedeemResetPassword.ts b/packages/components/src/api/user/RedeemResetPassword.ts
new file mode 100644
index 0000000000..285dc49826
--- /dev/null
+++ b/packages/components/src/api/user/RedeemResetPassword.ts
@@ -0,0 +1,57 @@
+import { ApiConfig } from "@storyteller/components";
+
+export interface RequestResetPasswordRequest {
+ reset_token: string,
+ new_password: string,
+ new_password_validation: string,
+}
+
+export interface RequestResetPasswordSuccessResponse {
+ success: boolean,
+}
+
+export interface RequestResetPasswordErrorResponse {
+ success: boolean,
+ error_type: string,
+ error_fields: { [key: string]: string; },
+}
+
+type RequestResetPasswordResponse = RequestResetPasswordSuccessResponse | RequestResetPasswordErrorResponse;
+
+export function RequestResetPasswordIsSuccess(response: RequestResetPasswordResponse): response is RequestResetPasswordSuccessResponse {
+ return response?.success === true;
+}
+
+export function RequestResetPasswordIsError(response: RequestResetPasswordResponse): response is RequestResetPasswordErrorResponse {
+ return response?.success === false;
+}
+
+export async function RequestResetPassword(request: RequestResetPasswordRequest) : Promise
+{
+ const endpoint = new ApiConfig().passwordResetRedeem();
+
+ return await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify(request),
+ })
+ .then(res => res.json())
+ .then(res => {
+ if (!res) {
+ return { success : false };
+ }
+
+ if (res && 'success' in res) {
+ return res;
+ } else {
+ return { success : false };
+ }
+ })
+ .catch(e => {
+ return { success : false };
+ });
+}
diff --git a/packages/components/src/api/user/RequestResetPassword.ts b/packages/components/src/api/user/RequestResetPassword.ts
new file mode 100644
index 0000000000..b6eb45cb12
--- /dev/null
+++ b/packages/components/src/api/user/RequestResetPassword.ts
@@ -0,0 +1,55 @@
+import { ApiConfig } from "@storyteller/components";
+
+export interface RequestResetPasswordRequest {
+ username_or_email: string,
+}
+
+export interface RequestResetPasswordSuccessResponse {
+ success: boolean,
+}
+
+export interface RequestResetPasswordErrorResponse {
+ success: boolean,
+ error_type: string,
+ error_fields: { [key: string]: string; },
+}
+
+type RequestResetPasswordResponse = RequestResetPasswordSuccessResponse | RequestResetPasswordErrorResponse;
+
+export function RequestResetPasswordIsSuccess(response: RequestResetPasswordResponse): response is RequestResetPasswordSuccessResponse {
+ return response?.success === true;
+}
+
+export function RequestResetPasswordIsError(response: RequestResetPasswordResponse): response is RequestResetPasswordErrorResponse {
+ return response?.success === false;
+}
+
+export async function RequestResetPassword(request: RequestResetPasswordRequest) : Promise
+{
+ const endpoint = new ApiConfig().passwordResetRequest();
+
+ return await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify(request),
+ })
+ .then(res => res.json())
+ .then(res => {
+ if (!res) {
+ return { success : false };
+ }
+
+ if (res && 'success' in res) {
+ return res;
+ } else {
+ return { success : false };
+ }
+ })
+ .catch(e => {
+ return { success : false };
+ });
+}
diff --git a/packages/fakeyou.com/src/v2/view/pages/login/LoginPage.tsx b/packages/fakeyou.com/src/v2/view/pages/login/LoginPage.tsx
index a48d4400ae..1135cc46e3 100644
--- a/packages/fakeyou.com/src/v2/view/pages/login/LoginPage.tsx
+++ b/packages/fakeyou.com/src/v2/view/pages/login/LoginPage.tsx
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
-import { useHistory } from "react-router-dom";
+import { Link, useHistory } from "react-router-dom";
import {
CreateSession,
CreateSessionIsError,
@@ -132,11 +132,11 @@ function LoginPage(props: Props) {
onChange={handlePasswordChange}
/>
- {/*
+
Forgot your password?
-
*/}
+
diff --git a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetEmailPage.tsx b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetEmailPage.tsx
index c28aa8d728..a8b6244938 100644
--- a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetEmailPage.tsx
+++ b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetEmailPage.tsx
@@ -1,6 +1,6 @@
-import React from "react";
+import React, { useState } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
-import { useHistory } from "react-router-dom";
+import { Link, useHistory } from "react-router-dom";
import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocumentTitle";
import Container from "components/common/Container";
import PageHeader from "components/layout/PageHeader";
@@ -8,6 +8,9 @@ import Input from "components/common/Input";
import { Button } from "components/common";
import Panel from "components/common/Panel";
import { faEnvelope } from "@fortawesome/pro-solid-svg-icons";
+import {
+ RequestResetPassword, RequestResetPasswordIsSuccess,
+} from "@storyteller/components/src/api/user/RequestResetPassword";
interface Props {
sessionWrapper: SessionWrapper;
@@ -16,11 +19,61 @@ interface Props {
function PasswordResetEmailPage(props: Props) {
let history = useHistory();
+ usePrefixedDocumentTitle("Reset Password");
+
+ const [emailOrUsername, setEmailOrUsername] = useState("");
+ const [isSent, setIsSent] = useState(false);
+
if (props.sessionWrapper.isLoggedIn()) {
history.push("/");
}
- usePrefixedDocumentTitle("Reset Password");
+ const handleChange = (
+ ev: React.FormEvent
+ ) => {
+ const value = (ev.target as HTMLInputElement).value;
+ setEmailOrUsername(value);
+ }
+
+ const handleSubmit = async (
+ ev: React.FormEvent
+ ) : Promise => {
+ ev.preventDefault();
+
+ const value = emailOrUsername.trim();
+
+ if (value.length < 4) {
+ return false;
+ }
+
+ const request = {
+ username_or_email: value
+ };
+
+ const response = await RequestResetPassword(request);
+
+ if (RequestResetPasswordIsSuccess(response)) {
+ setIsSent(true);
+ }
+
+ return false;
+ }
+
+ if (isSent) {
+ return (
+
+
+
+ Click here once you have the code.
+
+
+ );
+ }
return (
@@ -36,12 +89,14 @@ function PasswordResetEmailPage(props: Props) {
From bcb232301a3173d6be4fa63796a257cbaabb2e79 Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Sun, 12 Nov 2023 04:08:59 -0500
Subject: [PATCH 2/6] form validation
---
.../PasswordResetVerificationPage.tsx | 143 ++++++++++++++++--
1 file changed, 131 insertions(+), 12 deletions(-)
diff --git a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
index 12a7b756f2..df5cbe22ec 100644
--- a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
+++ b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
@@ -15,44 +15,141 @@ interface Props {
function PasswordResetVerificationPage(props: Props) {
let history = useHistory();
- const [isLoading, setIsLoading] = useState(false);
+
+ usePrefixedDocumentTitle("Password Reset Verification");
+
+ const [resetToken, setResetToken] = useState(getCodeFromUrl() || "");
+ const [resetTokenLooksValid, setResetTokenLooksValid] = useState(!!!getResetCodeErrors(getCodeFromUrl()));
+ const [resetTokenInvalidReason, setResetTokenInvalidReason] = useState(getResetCodeErrors(getCodeFromUrl()));
+
+ const [newPassword, setNewPassword] = useState("");
+ const [newPasswordIsValid, setNewPasswordIsValid] = useState(false);
+ const [newPasswordInvalidReason, setNewPasswordInvalidReason] = useState("new password is too short");
+
+ const [newPasswordConfirmation, setNewPasswordConfirmation] = useState("");
+ const [newPasswordConfirmationIsValid, setNewPasswordConfirmationIsValid] = useState(false);
+ const [newPasswordConfirmationInvalidReason, setNewPasswordConfirmationInvalidReason] = useState("new password is too short");
if (props.sessionWrapper.isLoggedIn()) {
history.push("/");
}
- usePrefixedDocumentTitle("Code Verification");
+ const handleChangeResetToken = (ev: React.FormEvent) => {
+ const token = (ev.target as HTMLInputElement).value;
+ const errors = getResetCodeErrors(token);
+ setResetToken(token);
+ setResetTokenLooksValid(!!!errors);
+ setResetTokenInvalidReason(errors);
+ }
+
+ const handleChangePassword = (ev: React.FormEvent) => {
+ const value = (ev.target as HTMLInputElement).value;
+
+ let isValid = true;
+ let invalidReason = undefined;
+
+ if (value.length < 5) {
+ isValid = false;
+ invalidReason = "new password is too short";
+ }
+
+ setNewPassword(value);
+ setNewPasswordIsValid(isValid);
+ setNewPasswordInvalidReason(invalidReason)
+
+ if (value !== newPasswordConfirmation) {
+ setNewPasswordConfirmationIsValid(false);
+ setNewPasswordConfirmationInvalidReason("new password does not match")
+ } else if (newPasswordConfirmation.length > 4) {
+ setNewPasswordConfirmationIsValid(true);
+ setNewPasswordConfirmationInvalidReason(undefined)
+ }
+ }
+
+ const handleChangePasswordConfirmation = (ev: React.FormEvent) => {
+ const value = (ev.target as HTMLInputElement).value;
+
+ let isValid = true;
+ let invalidReason = undefined;
+
+ if (value !== newPassword) {
+ isValid = false;
+ invalidReason = "new password does not match";
+ }
+ else if (value.length < 5) {
+ isValid = false;
+ invalidReason = "new password is too short";
+ }
+
+ setNewPasswordConfirmation(value);
+ setNewPasswordConfirmationIsValid(isValid);
+ setNewPasswordConfirmationInvalidReason(invalidReason)
+ }
+
const handleVerifyCode = async () => {
- // Simulate a delay using a timer (dummy timer)
- setIsLoading(true);
-
- setTimeout(() => {
- setIsLoading(false);
- }, 2000);
};
+ const canSubmit = resetTokenLooksValid && newPasswordIsValid && newPasswordConfirmationIsValid;
+
+ let resetTokenHelpClasses = resetTokenLooksValid ? "" : "form-control is-danger";
+ let newPasswordHelpClasses = newPasswordIsValid ? "" : "form-control is-danger";
+ let newPasswordConfirmationHelpClasses = newPasswordConfirmationIsValid ? "" : "form-control is-danger";
+
return (
@@ -61,4 +158,26 @@ function PasswordResetVerificationPage(props: Props) {
);
}
+function getCodeFromUrl() : string | null {
+ const urlParams = new URLSearchParams(window.location.search);
+ return urlParams.get('token');
+}
+
+function checkResetCodeLooksValid(code: string | null) : boolean {
+ if (!code) {
+ return false;
+ }
+ return code.length > 10;
+}
+
+function getResetCodeErrors(code: string | null) : string | undefined {
+ if (!code) {
+ return "no code set";
+ }
+ if (code.length < 10) {
+ return "code is too short";
+ };
+}
+
+
export { PasswordResetVerificationPage };
From 21b8fffe08431744058c5673f3c8c573f56fb452 Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Sun, 12 Nov 2023 04:18:55 -0500
Subject: [PATCH 3/6] redemption request
---
.../src/api/user/RedeemResetPassword.ts | 16 ++++----
.../PasswordResetVerificationPage.tsx | 37 ++++++++++++++-----
2 files changed, 36 insertions(+), 17 deletions(-)
diff --git a/packages/components/src/api/user/RedeemResetPassword.ts b/packages/components/src/api/user/RedeemResetPassword.ts
index 285dc49826..2f043c089e 100644
--- a/packages/components/src/api/user/RedeemResetPassword.ts
+++ b/packages/components/src/api/user/RedeemResetPassword.ts
@@ -1,32 +1,32 @@
import { ApiConfig } from "@storyteller/components";
-export interface RequestResetPasswordRequest {
+export interface RedeemResetPasswordRedeem {
reset_token: string,
new_password: string,
new_password_validation: string,
}
-export interface RequestResetPasswordSuccessResponse {
+export interface RedeemResetPasswordSuccessResponse {
success: boolean,
}
-export interface RequestResetPasswordErrorResponse {
+export interface RedeemResetPasswordErrorResponse {
success: boolean,
error_type: string,
error_fields: { [key: string]: string; },
}
-type RequestResetPasswordResponse = RequestResetPasswordSuccessResponse | RequestResetPasswordErrorResponse;
+type RedeemResetPasswordResponse = RedeemResetPasswordSuccessResponse | RedeemResetPasswordErrorResponse;
-export function RequestResetPasswordIsSuccess(response: RequestResetPasswordResponse): response is RequestResetPasswordSuccessResponse {
+export function RedeemResetPasswordIsSuccess(response: RedeemResetPasswordResponse): response is RedeemResetPasswordSuccessResponse {
return response?.success === true;
}
-export function RequestResetPasswordIsError(response: RequestResetPasswordResponse): response is RequestResetPasswordErrorResponse {
+export function RedeemResetPasswordIsError(response: RedeemResetPasswordResponse): response is RedeemResetPasswordErrorResponse {
return response?.success === false;
}
-export async function RequestResetPassword(request: RequestResetPasswordRequest) : Promise
+export async function RedeemResetPassword(Redeem: RedeemResetPasswordRedeem) : Promise
{
const endpoint = new ApiConfig().passwordResetRedeem();
@@ -37,7 +37,7 @@ export async function RequestResetPassword(request: RequestResetPasswordRequest)
'Content-Type': 'application/json',
},
credentials: 'include',
- body: JSON.stringify(request),
+ body: JSON.stringify(Redeem),
})
.then(res => res.json())
.then(res => {
diff --git a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
index df5cbe22ec..b7d256bac6 100644
--- a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
+++ b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
@@ -8,6 +8,9 @@ import Input from "components/common/Input";
import { Button } from "components/common";
import Panel from "components/common/Panel";
import { faLock } from "@fortawesome/pro-solid-svg-icons";
+import {
+ RedeemResetPassword, RedeemResetPasswordIsSuccess,
+} from "@storyteller/components/src/api/user/RedeemResetPassword";
interface Props {
sessionWrapper: SessionWrapper;
@@ -86,8 +89,29 @@ function PasswordResetVerificationPage(props: Props) {
setNewPasswordConfirmationInvalidReason(invalidReason)
}
+ const handleSubmit = async (
+ ev: React.FormEvent
+ ) : Promise => {
+ ev.preventDefault();
- const handleVerifyCode = async () => {
+ const password = newPassword.trim();
+ const passwordConfirmation = newPasswordConfirmation.trim();
+
+ const request = {
+ reset_token: resetToken,
+ new_password: password,
+ new_password_validation: passwordConfirmation,
+ };
+
+ const response = await RedeemResetPassword(request);
+
+ // TODO(bt,2023-11-12): Handle server-side errors
+
+ if (RedeemResetPasswordIsSuccess(response)) {
+ history.push("/");
+ }
+
+ return false;
};
const canSubmit = resetTokenLooksValid && newPasswordIsValid && newPasswordConfirmationIsValid;
@@ -148,7 +172,7 @@ function PasswordResetVerificationPage(props: Props) {
@@ -158,18 +182,13 @@ function PasswordResetVerificationPage(props: Props) {
);
}
+// Pre-load the code from a URL query string, eg https://fakeyou.com/password-reset/validate?code=codeGoesHere
function getCodeFromUrl() : string | null {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('token');
}
-function checkResetCodeLooksValid(code: string | null) : boolean {
- if (!code) {
- return false;
- }
- return code.length > 10;
-}
-
+// Handle error state at initialization
function getResetCodeErrors(code: string | null) : string | undefined {
if (!code) {
return "no code set";
From cbe96fc88b671b7f6da609dfead0bef6dcec7b98 Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Sun, 12 Nov 2023 04:35:07 -0500
Subject: [PATCH 4/6] query session, error strings consolidated
---
.../fakeyou.com/src/v2/view/PageContainer.tsx | 11 +--
.../PasswordResetChangePage.tsx | 80 -------------------
.../PasswordResetVerificationPage.tsx | 28 ++++---
3 files changed, 23 insertions(+), 96 deletions(-)
delete mode 100644 packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetChangePage.tsx
diff --git a/packages/fakeyou.com/src/v2/view/PageContainer.tsx b/packages/fakeyou.com/src/v2/view/PageContainer.tsx
index edce3e7bdf..03cd213e71 100644
--- a/packages/fakeyou.com/src/v2/view/PageContainer.tsx
+++ b/packages/fakeyou.com/src/v2/view/PageContainer.tsx
@@ -97,7 +97,6 @@ import { VoiceDesignerVoiceEditPage } from "./pages/voice_designer/VoiceDesigner
import VoiceDesignerUseVoicePage from "./pages/voice_designer/VoiceDesignerUseVoicePage";
import { PasswordResetEmailPage } from "./pages/password_reset/PasswordResetEmailPage";
import { PasswordResetVerificationPage } from "./pages/password_reset/PasswordResetVerificationPage";
-import { PasswordResetChangePage } from "./pages/password_reset/PasswordResetChangePage";
interface Props {
sessionWrapper: SessionWrapper;
@@ -274,15 +273,13 @@ class PageContainer extends React.Component<
/>
-
-
-
-
diff --git a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetChangePage.tsx b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetChangePage.tsx
deleted file mode 100644
index f8e1127982..0000000000
--- a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetChangePage.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import React, { useState } from "react";
-import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
-import { useHistory } from "react-router-dom";
-import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocumentTitle";
-import Container from "components/common/Container";
-import PageHeader from "components/layout/PageHeader";
-import Input from "components/common/Input";
-import { Button } from "components/common";
-import Panel from "components/common/Panel";
-import { faKey } from "@fortawesome/pro-solid-svg-icons";
-import BackButton from "components/common/BackButton";
-
-interface Props {
- sessionWrapper: SessionWrapper;
-}
-
-function PasswordResetChangePage(props: Props) {
- let history = useHistory();
- const [isPasswordChanged, setIsPasswordChanged] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
-
- if (props.sessionWrapper.isLoggedIn()) {
- history.push("/");
- }
-
- usePrefixedDocumentTitle("Reset Password");
-
- const handlePasswordChange = async () => {
- // Simulate a delay using a timer (dummy timer)
- setIsLoading(true);
-
- setTimeout(() => {
- setIsPasswordChanged(true);
- setIsLoading(false);
- }, 2000);
- };
-
- return (
-
-
-
-
- {isPasswordChanged ? (
-
-
Your password has been successfully changed!
-
-
- ) : (
-
- )}
-
-
- );
-}
-
-export { PasswordResetChangePage };
diff --git a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
index b7d256bac6..c085cdae3d 100644
--- a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
+++ b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
@@ -14,8 +14,16 @@ import {
interface Props {
sessionWrapper: SessionWrapper;
+ querySessionAction: () => void;
+ querySessionSubscriptionsAction: () => void;
}
+// TODO(bt,2023-11-12): Localize error messages
+const ERR_CODE_NOT_SET = "password reset code is not set";
+const ERR_CODE_TOO_SHORT = "password reset code is too short";
+const ERR_PASSWORD_TOO_SHORT = "new password is too short";
+const ERR_PASSWORD_DOES_NOT_MATCH = "new password does not match";
+
function PasswordResetVerificationPage(props: Props) {
let history = useHistory();
@@ -27,11 +35,11 @@ function PasswordResetVerificationPage(props: Props) {
const [newPassword, setNewPassword] = useState("");
const [newPasswordIsValid, setNewPasswordIsValid] = useState(false);
- const [newPasswordInvalidReason, setNewPasswordInvalidReason] = useState("new password is too short");
+ const [newPasswordInvalidReason, setNewPasswordInvalidReason] = useState(ERR_PASSWORD_TOO_SHORT);
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState("");
const [newPasswordConfirmationIsValid, setNewPasswordConfirmationIsValid] = useState(false);
- const [newPasswordConfirmationInvalidReason, setNewPasswordConfirmationInvalidReason] = useState("new password is too short");
+ const [newPasswordConfirmationInvalidReason, setNewPasswordConfirmationInvalidReason] = useState(ERR_PASSWORD_TOO_SHORT);
if (props.sessionWrapper.isLoggedIn()) {
history.push("/");
@@ -53,7 +61,7 @@ function PasswordResetVerificationPage(props: Props) {
if (value.length < 5) {
isValid = false;
- invalidReason = "new password is too short";
+ invalidReason = ERR_PASSWORD_TOO_SHORT;
}
setNewPassword(value);
@@ -62,7 +70,7 @@ function PasswordResetVerificationPage(props: Props) {
if (value !== newPasswordConfirmation) {
setNewPasswordConfirmationIsValid(false);
- setNewPasswordConfirmationInvalidReason("new password does not match")
+ setNewPasswordConfirmationInvalidReason(ERR_PASSWORD_DOES_NOT_MATCH)
} else if (newPasswordConfirmation.length > 4) {
setNewPasswordConfirmationIsValid(true);
setNewPasswordConfirmationInvalidReason(undefined)
@@ -77,11 +85,11 @@ function PasswordResetVerificationPage(props: Props) {
if (value !== newPassword) {
isValid = false;
- invalidReason = "new password does not match";
+ invalidReason = ERR_PASSWORD_DOES_NOT_MATCH;
}
else if (value.length < 5) {
isValid = false;
- invalidReason = "new password is too short";
+ invalidReason = ERR_PASSWORD_TOO_SHORT;
}
setNewPasswordConfirmation(value);
@@ -108,6 +116,8 @@ function PasswordResetVerificationPage(props: Props) {
// TODO(bt,2023-11-12): Handle server-side errors
if (RedeemResetPasswordIsSuccess(response)) {
+ props.querySessionAction();
+ props.querySessionSubscriptionsAction();
history.push("/");
}
@@ -135,7 +145,7 @@ function PasswordResetVerificationPage(props: Props) {
@@ -191,10 +201,10 @@ function getCodeFromUrl() : string | null {
// Handle error state at initialization
function getResetCodeErrors(code: string | null) : string | undefined {
if (!code) {
- return "no code set";
+ return ERR_CODE_NOT_SET;
}
if (code.length < 10) {
- return "code is too short";
+ return ERR_CODE_TOO_SHORT;
};
}
From f9c1b5671c2731380cfa22ddaf69c49d1109dc82 Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Sun, 12 Nov 2023 04:41:54 -0500
Subject: [PATCH 5/6] prevent injection
---
.../pages/password_reset/PasswordResetVerificationPage.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
index c085cdae3d..bc4a99b372 100644
--- a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
+++ b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
@@ -195,7 +195,9 @@ function PasswordResetVerificationPage(props: Props) {
// Pre-load the code from a URL query string, eg https://fakeyou.com/password-reset/validate?code=codeGoesHere
function getCodeFromUrl() : string | null {
const urlParams = new URLSearchParams(window.location.search);
- return urlParams.get('token');
+ const tokenUnsafe = urlParams.get('token');
+ const tokenSafe = tokenUnsafe === null ? null : tokenUnsafe.replace(/[^A-Za-z0-9]/g, '');
+ return tokenSafe;
}
// Handle error state at initialization
From 7f0fb3d932a68a9f3db05dbda030c3597e18719b Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Sun, 12 Nov 2023 05:43:48 -0500
Subject: [PATCH 6/6] final error states
---
.../password_reset/PasswordResetVerificationPage.tsx | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
index bc4a99b372..2140abf903 100644
--- a/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
+++ b/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx
@@ -23,6 +23,7 @@ const ERR_CODE_NOT_SET = "password reset code is not set";
const ERR_CODE_TOO_SHORT = "password reset code is too short";
const ERR_PASSWORD_TOO_SHORT = "new password is too short";
const ERR_PASSWORD_DOES_NOT_MATCH = "new password does not match";
+const ERR_BACKEND = "There was an issue resetting your password. Perhaps your code expired?";
function PasswordResetVerificationPage(props: Props) {
let history = useHistory();
@@ -41,6 +42,8 @@ function PasswordResetVerificationPage(props: Props) {
const [newPasswordConfirmationIsValid, setNewPasswordConfirmationIsValid] = useState(false);
const [newPasswordConfirmationInvalidReason, setNewPasswordConfirmationInvalidReason] = useState(ERR_PASSWORD_TOO_SHORT);
+ const [backendError, setBackendError] = useState(undefined);
+
if (props.sessionWrapper.isLoggedIn()) {
history.push("/");
}
@@ -116,9 +119,12 @@ function PasswordResetVerificationPage(props: Props) {
// TODO(bt,2023-11-12): Handle server-side errors
if (RedeemResetPasswordIsSuccess(response)) {
+ setBackendError(undefined);
props.querySessionAction();
props.querySessionSubscriptionsAction();
history.push("/");
+ } else {
+ setBackendError(ERR_BACKEND);
}
return false;
@@ -129,6 +135,7 @@ function PasswordResetVerificationPage(props: Props) {
let resetTokenHelpClasses = resetTokenLooksValid ? "" : "form-control is-danger";
let newPasswordHelpClasses = newPasswordIsValid ? "" : "form-control is-danger";
let newPasswordConfirmationHelpClasses = newPasswordConfirmationIsValid ? "" : "form-control is-danger";
+ let backendErrorClasses = !!!backendError ? "" : "form-control is-danger";
return (
@@ -180,6 +187,10 @@ function PasswordResetVerificationPage(props: Props) {
{newPasswordConfirmationInvalidReason}
+
+ {backendError}
+
+