From b1daae95eeb15205f92030089dd4c0f1152a372a Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 12 Nov 2023 03:18:30 -0500 Subject: [PATCH] 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) {
-