Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions backend-dummy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,24 @@ This backend does not use a real database. However, there is a file named users.

- **200 OK:** Returns the authenticated user's information.
- **401 Unauthorized:** The user is not authenticated or lacks permission to perform this action.

* [POST] users/forgotPassword

**Description**

This endpoint initiates the process of resetting a user's password. The user must provide their email address in the request body. If the email is associated with an account, a token will be created.

**Responses:**

- **200 OK:** If the email provided exists in our database we print the token in the console.

* [POST] users/setPassword

**Description**

Receives a token and updates the associated user's password.

**Responses:**

- **200 OK:** The user's password has been successfully updated.
- **400 Bad Request:** Missing required fields in the request body or invalid token.
43 changes: 43 additions & 0 deletions backend-dummy/routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,47 @@ router.get("/me", function (req, res, next) {
return res.json({ status: "success", name: name, email: email });
});

router.post("/forgotPassword", function (req, res, next) {
const { email } = req.body;
if (!email) {
return res
.status(400)
.json({ status: "error", message: "Invalid form submission", code: 400 });
}
const user = users.find((user) => user.email === email);
if (user) {
const token = randomUUID();
user["token"] = token;
fs.writeFileSync("users.json", `{"users":${JSON.stringify(users)}}`);
console.log("Token to do the reset password:", token);
}
return res.json({
status: "success",
message: "Token sent",
});
});

router.post("/setPassword", function (req, res, next) {
const { token, password } = req.body;
if (!token || !password) {
return res.status(400).json({
status: "error",
message: "Invalid form submission or token",
code: 400,
});
}
const user = users.find((user) => user.token === token);
if (!user) {
return res.status(400).json({
status: "error",
message: "Invalid form submission or token",
code: 400,
});
}
user["password"] = password;
delete user["token"];
fs.writeFileSync("users.json", `{"users":${JSON.stringify(users)}}`);
return res.json({ status: "success", message: "Password updated" });
});

module.exports = router;
2 changes: 2 additions & 0 deletions src/networking/api-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const API_ROUTES = {
LOGIN: "users/login",
SIGN_UP: "users/signUp",
ME: "users/me",
FORGOT_PASSWORD: "users/forgotPassword",
SET_PASSWORD: "users/setPassword",
};

export { API_ROUTES };
16 changes: 15 additions & 1 deletion src/networking/controllers/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,18 @@ const me = async () => {
return info;
};

export { login, signUp, me };
const setNewPassword = async (token: string, password: string) => {
const response = await ApiService.post(API_ROUTES.SET_PASSWORD, {
body: JSON.stringify({ token, password }),
});
return response;
};

const forgotPassword = async (email: string) => {
const response = await ApiService.post(API_ROUTES.FORGOT_PASSWORD, {
body: JSON.stringify({ email }),
});
return response;
};

export { login, signUp, me, setNewPassword, forgotPassword };
32 changes: 32 additions & 0 deletions src/pages/forgot-password/forgot-password.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@use "../../assets/stylesheets/text-styles.scss";
@import "../../assets/stylesheets/colors";

.container {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
padding: 0 30px;
}

.form {
max-width: 400px;
width: 100%;
padding: 30px;
border: 1px solid $text-neutral-20;
border-radius: 5px;
}

.field {
margin-bottom: 20px;
}

.submitButton {
width: 150px;
margin: 0 auto;
}

.message {
color: $primary-color-40;
}
54 changes: 54 additions & 0 deletions src/pages/forgot-password/forgot-password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { TextField } from "common/text-field";
import { useState } from "react";
import { Button } from "common/button";
import { forgotPassword } from "networking/controllers/users";
import styles from "./forgot-password.module.scss";

const ForgotPassword = () => {
const [error, setError] = useState<boolean>(false);
const [showSuccess, setShowSuccess] = useState<boolean>(false);
const [email, setEmail] = useState<string>("");

const handleSendMail = async () => {
try {
await forgotPassword(email);
setShowSuccess(true);
} catch (e) {
setError(true);
}
};

const doSendMail = () => {
setError(false);
setShowSuccess(false);
handleSendMail().catch(() => {
setError(true);
});
};

return (
<div className={styles.container}>
<form className={styles.form}>
<TextField
className={styles.field}
label="Email"
name="email"
onChange={(e) => {
setEmail(e.target.value);
}}
/>
{error && (
<p className={styles.message}>
Something went wrong. Please try again.
</p>
)}
{showSuccess && <p className={styles.message}>Email sent!</p>}
<Button className={styles.submitButton} onClick={doSendMail}>
Send email
</Button>
</form>
</div>
);
};

export { ForgotPassword };
3 changes: 3 additions & 0 deletions src/pages/forgot-password/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ForgotPassword } from "./forgot-password";

export { ForgotPassword };
8 changes: 8 additions & 0 deletions src/pages/login/login.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,11 @@
.error {
color: $primary-color-40;
}

.buttonContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
}
24 changes: 15 additions & 9 deletions src/pages/login/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Button } from "common/button";
import { TextField } from "common/text-field";
import styles from "./login.module.scss";
import { login } from "networking/controllers/users";
import { useNavigate } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { RouteName } from "routes";

export const Login = () => {
const navigate = useNavigate();
Expand Down Expand Up @@ -48,14 +49,19 @@ export const Login = () => {
setPassword(e.target.value);
}}
/>
{error && <p className={styles.error}>Incorrect email or password.</p>}
<Button
className={styles.submitButton}
disabled={!formValid}
onClick={doLogin}
>
Submit
</Button>
<div className={styles.buttonContainer}>
{error && (
<p className={styles.error}>Incorrect email or password.</p>
)}
<Button
className={styles.submitButton}
disabled={!formValid}
onClick={doLogin}
>
Submit
</Button>
<Link to={`/${RouteName.ForgotPassword}`}>Forgot password?</Link>
</div>
</form>
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions src/pages/reset-password/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ResetPassword } from "./reset-password";

export { ResetPassword };
33 changes: 33 additions & 0 deletions src/pages/reset-password/reset-password.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@use "../../assets/stylesheets/text-styles.scss";
@import "../../assets/stylesheets/colors";

.container {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
padding: 0 30px;
}

.form {
max-width: 400px;
width: 100%;
padding: 30px;
border: 1px solid $text-neutral-20;
border-radius: 5px;
}

.field {
margin-bottom: 20px;
}

.submitButton {
width: 150px;
margin: 0 auto;
}

.message {
color: $primary-color-40;
margin-bottom: 20px;
}
102 changes: 102 additions & 0 deletions src/pages/reset-password/reset-password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { TextField } from "common/text-field";
import styles from "./reset-password.module.scss";
import { useEffect, useState } from "react";
import { Button } from "common/button";
import { setNewPassword } from "networking/controllers/users";
import { useGoToPage } from "hooks/use-go-to-page";
import { RouteName } from "routes";

export const ResetPassword = () => {
const goToPage = useGoToPage();
const [password, setPassword] = useState<string>("");
const [token, setToken] = useState<string>("");
const [repeatPassword, setRepeatPassword] = useState<string>("");
const [passwordError, setPasswordError] = useState<boolean>(false);
const [error, setError] = useState<boolean>(false);
const [success, setSuccess] = useState<boolean>(false);
const formValid = !passwordError && password && repeatPassword;

useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const tokenParameter = urlParams.get("token");
if (tokenParameter) {
setToken(tokenParameter);
} else {
goToPage(RouteName.Login);
}
}, [goToPage]);

const handleReset = async () => {
try {
await setNewPassword(token, password);
setSuccess(true);
} catch (e) {
setError(true);
}
};

const doReset = () => {
handleReset().catch(() => {
setError(true);
});
};
return (
<div className={styles.container}>
<form className={styles.form}>
<TextField
className={styles.field}
label="Password"
name="password"
type="password"
onChange={(e) => {
setPassword(e.target.value);
}}
onBlur={() => {
if (repeatPassword && password !== repeatPassword) {
setPasswordError(true);
} else {
setPasswordError(false);
}
}}
/>
<TextField
className={styles.field}
label="Repeat Password"
name="Repeat password"
type="password"
onChange={(e) => {
setRepeatPassword(e.target.value);
}}
onBlur={() => {
if (password !== repeatPassword) {
setPasswordError(true);
} else {
setPasswordError(false);
}
}}
/>
{passwordError && (
<div className={styles.message}>The passwords do not match.</div>
)}

{error && (
<div className={styles.message}>
Something went wrong. Please try again.
</div>
)}
{success && (
<div className={styles.message}>
Your password has been changed successfully.
</div>
)}
<Button
className={styles.submitButton}
disabled={!formValid}
onClick={doReset}
>
Change password
</Button>
</form>
</div>
);
};
Loading