Skip to content

Commit a0c471a

Browse files
committed
chore: add captcha to the forgot password modal
1 parent a2d91f2 commit a0c471a

File tree

9 files changed

+126
-50
lines changed

9 files changed

+126
-50
lines changed

backend/__tests__/api/controllers/user.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,11 @@ describe("user controller test", () => {
426426
AuthUtils,
427427
"sendForgotPasswordEmail"
428428
);
429+
const verifyCaptchaMock = vi.spyOn(Captcha, "verify");
430+
429431
beforeEach(() => {
430432
sendForgotPasswordEmailMock.mockReset().mockResolvedValue();
433+
verifyCaptchaMock.mockReset().mockResolvedValue(true);
431434
});
432435

433436
it("should send forgot password email without authentication", async () => {
@@ -436,7 +439,7 @@ describe("user controller test", () => {
436439
//WHEN
437440
const { body } = await mockApp
438441
.post("/users/forgotPasswordEmail")
439-
.send({ email: "[email protected]" });
442+
.send({ email: "[email protected]", captcha: "" });
440443

441444
//THEN
442445
expect(body).toEqual({
@@ -458,7 +461,7 @@ describe("user controller test", () => {
458461
//THEN
459462
expect(body).toEqual({
460463
message: "Invalid request data schema",
461-
validationErrors: ['"email" Required'],
464+
validationErrors: ['"captcha" Required', '"email" Required'],
462465
});
463466
});
464467
it("should fail without unknown properties", async () => {
@@ -471,7 +474,10 @@ describe("user controller test", () => {
471474
//THEN
472475
expect(body).toEqual({
473476
message: "Invalid request data schema",
474-
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
477+
validationErrors: [
478+
'"captcha" Required',
479+
"Unrecognized key(s) in object: 'extra'",
480+
],
475481
});
476482
});
477483
});

backend/src/api/controllers/user.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ export async function sendVerificationEmail(
238238
export async function sendForgotPasswordEmail(
239239
req: MonkeyRequest<undefined, ForgotPasswordEmailRequest>
240240
): Promise<MonkeyResponse> {
241-
const { email } = req.body;
241+
const { email, captcha } = req.body;
242+
await verifyCaptcha(captcha);
242243
await authSendForgotPasswordEmail(email);
243244
return new MonkeyResponse(
244245
"Password reset request received. If the email is valid, you will receive an email shortly.",

frontend/src/html/popups.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
</div>
55
</dialog>
66

7+
<dialog id="forgotPasswordModal" class="modalWrapper hidden">
8+
<div class="modal">
9+
<div class="title">Forgot password</div>
10+
<input type="text" placeholder="email" />
11+
<div class="g-recaptcha"></div>
12+
<!-- <button>send</button> -->
13+
</div>
14+
</dialog>
15+
716
<dialog id="miniResultChartModal" class="modalWrapper hidden">
817
<div class="modal">
918
<canvas></canvas>

frontend/src/styles/popups.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ body.darkMode {
9191
}
9292
}
9393

94+
#forgotPasswordModal {
95+
.modal {
96+
max-width: 400px;
97+
}
98+
}
99+
94100
#customTextModal {
95101
.modal {
96102
max-width: 1200px;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as ForgotPasswordModal from "../modals/forgot-password";
2+
3+
const loginPage = document.querySelector("#pageLogin") as HTMLElement;
4+
5+
$(loginPage).on("click", "#forgotPasswordButton", () => {
6+
ForgotPasswordModal.show();
7+
});

frontend/src/ts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import "./event-handlers/test";
99
import "./event-handlers/about";
1010
import "./event-handlers/settings";
1111
import "./event-handlers/account";
12+
import "./event-handlers/login";
1213

1314
import "./modals/google-sign-up";
1415

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as CaptchaController from "../controllers/captcha-controller";
2+
import AnimatedModal from "../utils/animated-modal";
3+
import Ape from "../ape/index";
4+
import * as Notifications from "../elements/notifications";
5+
import * as Loader from "../elements/loader";
6+
import { z } from "zod";
7+
8+
export function show(): void {
9+
void modal.show({
10+
mode: "dialog",
11+
focusFirstInput: true,
12+
beforeAnimation: async (modal) => {
13+
CaptchaController.reset("forgotPasswordModal");
14+
CaptchaController.render(
15+
modal.querySelector(".g-recaptcha") as HTMLElement,
16+
"forgotPasswordModal",
17+
async () => {
18+
await submit();
19+
}
20+
);
21+
},
22+
});
23+
}
24+
25+
async function submit(): Promise<void> {
26+
const captchaResponse = CaptchaController.getResponse("forgotPasswordModal");
27+
if (!captchaResponse) {
28+
Notifications.add("Please complete the captcha");
29+
return;
30+
}
31+
32+
const email = (
33+
modal.getModal().querySelector("input") as HTMLInputElement
34+
).value.trim();
35+
36+
if (!email) {
37+
Notifications.add("Please enter your email address");
38+
CaptchaController.reset("forgotPasswordModal");
39+
return;
40+
}
41+
42+
const emailSchema = z.string().email();
43+
44+
const validation = emailSchema.safeParse(email);
45+
if (!validation.success) {
46+
Notifications.add("Please enter a valid email address");
47+
CaptchaController.reset("forgotPasswordModal");
48+
return;
49+
}
50+
51+
Loader.show();
52+
void Ape.users
53+
.forgotPasswordEmail({
54+
body: { email, captcha: captchaResponse },
55+
})
56+
.then((result) => {
57+
Loader.hide();
58+
if (result.status !== 200) {
59+
Notifications.add(
60+
"Failed to send password reset email: " + result.body.message,
61+
-1
62+
);
63+
return;
64+
}
65+
66+
Notifications.add(result.body.message, 1, { duration: 5 });
67+
});
68+
69+
hide();
70+
}
71+
72+
function hide(): void {
73+
void modal.hide();
74+
}
75+
76+
async function setup(modalEl: HTMLElement): Promise<void> {
77+
modalEl.querySelector("button")?.addEventListener("click", async () => {
78+
await submit();
79+
});
80+
}
81+
82+
const modal = new AnimatedModal({
83+
dialogId: "forgotPasswordModal",
84+
setup,
85+
customEscapeHandler: async (): Promise<void> => {
86+
hide();
87+
},
88+
customWrapperClickHandler: async (): Promise<void> => {
89+
hide();
90+
},
91+
});

frontend/src/ts/modals/simple-modals.ts

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ type PopupKey =
6060
| "resetProgressCustomTextLong"
6161
| "updateCustomTheme"
6262
| "deleteCustomTheme"
63-
| "forgotPassword"
6463
| "devGenerateData";
6564

6665
const list: Record<PopupKey, SimpleModal | undefined> = {
@@ -86,7 +85,6 @@ const list: Record<PopupKey, SimpleModal | undefined> = {
8685
resetProgressCustomTextLong: undefined,
8786
updateCustomTheme: undefined,
8887
deleteCustomTheme: undefined,
89-
forgotPassword: undefined,
9088
devGenerateData: undefined,
9189
};
9290

@@ -1221,46 +1219,6 @@ list.deleteCustomTheme = new SimpleModal({
12211219
},
12221220
});
12231221

1224-
list.forgotPassword = new SimpleModal({
1225-
id: "forgotPassword",
1226-
title: "Forgot password",
1227-
inputs: [
1228-
{
1229-
type: "text",
1230-
placeholder: "email",
1231-
initVal: "",
1232-
},
1233-
],
1234-
buttonText: "send",
1235-
execFn: async (_thisPopup, email): Promise<ExecReturn> => {
1236-
const result = await Ape.users.forgotPasswordEmail({
1237-
body: { email: email.trim() },
1238-
});
1239-
if (result.status !== 200) {
1240-
return {
1241-
status: -1,
1242-
message: "Failed to send password reset email: " + result.body.message,
1243-
};
1244-
}
1245-
1246-
return {
1247-
status: 1,
1248-
message: result.body.message,
1249-
notificationOptions: {
1250-
duration: 8,
1251-
},
1252-
};
1253-
},
1254-
beforeInitFn: (thisPopup): void => {
1255-
const inputValue = $(
1256-
`.pageLogin .login input[name="current-email"]`
1257-
).val() as string;
1258-
if (inputValue) {
1259-
(thisPopup.inputs[0] as TextInput).initVal = inputValue;
1260-
}
1261-
},
1262-
});
1263-
12641222
list.devGenerateData = new SimpleModal({
12651223
id: "devGenerateData",
12661224
title: "Generate data",
@@ -1365,10 +1323,6 @@ export function showPopup(
13651323
}
13661324

13671325
//todo: move these event handlers to their respective files (either global event files or popup files)
1368-
$(".pageLogin #forgotPasswordButton").on("click", () => {
1369-
showPopup("forgotPassword");
1370-
});
1371-
13721326
$(".pageAccountSettings").on("click", "#unlinkDiscordButton", () => {
13731327
showPopup("unlinkDiscord");
13741328
});

packages/contracts/src/users.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ export const ReportUserRequestSchema = z.object({
302302
export type ReportUserRequest = z.infer<typeof ReportUserRequestSchema>;
303303

304304
export const ForgotPasswordEmailRequestSchema = z.object({
305+
captcha: z.string(),
305306
email: z.string().email(),
306307
});
307308
export type ForgotPasswordEmailRequest = z.infer<

0 commit comments

Comments
 (0)