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) {