mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-09-07 07:10:01 +08:00
chore: add captcha to the forgot password modal
This commit is contained in:
parent
a2d91f2a73
commit
a0c471a28e
9 changed files with 126 additions and 50 deletions
|
@ -426,8 +426,11 @@ describe("user controller test", () => {
|
|||
AuthUtils,
|
||||
"sendForgotPasswordEmail"
|
||||
);
|
||||
const verifyCaptchaMock = vi.spyOn(Captcha, "verify");
|
||||
|
||||
beforeEach(() => {
|
||||
sendForgotPasswordEmailMock.mockReset().mockResolvedValue();
|
||||
verifyCaptchaMock.mockReset().mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("should send forgot password email without authentication", async () => {
|
||||
|
@ -436,7 +439,7 @@ describe("user controller test", () => {
|
|||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.post("/users/forgotPasswordEmail")
|
||||
.send({ email: "bob@example.com" });
|
||||
.send({ email: "bob@example.com", captcha: "" });
|
||||
|
||||
//THEN
|
||||
expect(body).toEqual({
|
||||
|
@ -458,7 +461,7 @@ describe("user controller test", () => {
|
|||
//THEN
|
||||
expect(body).toEqual({
|
||||
message: "Invalid request data schema",
|
||||
validationErrors: ['"email" Required'],
|
||||
validationErrors: ['"captcha" Required', '"email" Required'],
|
||||
});
|
||||
});
|
||||
it("should fail without unknown properties", async () => {
|
||||
|
@ -471,7 +474,10 @@ describe("user controller test", () => {
|
|||
//THEN
|
||||
expect(body).toEqual({
|
||||
message: "Invalid request data schema",
|
||||
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
|
||||
validationErrors: [
|
||||
'"captcha" Required',
|
||||
"Unrecognized key(s) in object: 'extra'",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -238,7 +238,8 @@ export async function sendVerificationEmail(
|
|||
export async function sendForgotPasswordEmail(
|
||||
req: MonkeyRequest<undefined, ForgotPasswordEmailRequest>
|
||||
): Promise<MonkeyResponse> {
|
||||
const { email } = req.body;
|
||||
const { email, captcha } = req.body;
|
||||
await verifyCaptcha(captcha);
|
||||
await authSendForgotPasswordEmail(email);
|
||||
return new MonkeyResponse(
|
||||
"Password reset request received. If the email is valid, you will receive an email shortly.",
|
||||
|
|
|
@ -4,6 +4,15 @@
|
|||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="forgotPasswordModal" class="modalWrapper hidden">
|
||||
<div class="modal">
|
||||
<div class="title">Forgot password</div>
|
||||
<input type="text" placeholder="email" />
|
||||
<div class="g-recaptcha"></div>
|
||||
<!-- <button>send</button> -->
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="miniResultChartModal" class="modalWrapper hidden">
|
||||
<div class="modal">
|
||||
<canvas></canvas>
|
||||
|
|
|
@ -91,6 +91,12 @@ body.darkMode {
|
|||
}
|
||||
}
|
||||
|
||||
#forgotPasswordModal {
|
||||
.modal {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
#customTextModal {
|
||||
.modal {
|
||||
max-width: 1200px;
|
||||
|
|
7
frontend/src/ts/event-handlers/login.ts
Normal file
7
frontend/src/ts/event-handlers/login.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import * as ForgotPasswordModal from "../modals/forgot-password";
|
||||
|
||||
const loginPage = document.querySelector("#pageLogin") as HTMLElement;
|
||||
|
||||
$(loginPage).on("click", "#forgotPasswordButton", () => {
|
||||
ForgotPasswordModal.show();
|
||||
});
|
|
@ -9,6 +9,7 @@ import "./event-handlers/test";
|
|||
import "./event-handlers/about";
|
||||
import "./event-handlers/settings";
|
||||
import "./event-handlers/account";
|
||||
import "./event-handlers/login";
|
||||
|
||||
import "./modals/google-sign-up";
|
||||
|
||||
|
|
91
frontend/src/ts/modals/forgot-password.ts
Normal file
91
frontend/src/ts/modals/forgot-password.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import * as CaptchaController from "../controllers/captcha-controller";
|
||||
import AnimatedModal from "../utils/animated-modal";
|
||||
import Ape from "../ape/index";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as Loader from "../elements/loader";
|
||||
import { z } from "zod";
|
||||
|
||||
export function show(): void {
|
||||
void modal.show({
|
||||
mode: "dialog",
|
||||
focusFirstInput: true,
|
||||
beforeAnimation: async (modal) => {
|
||||
CaptchaController.reset("forgotPasswordModal");
|
||||
CaptchaController.render(
|
||||
modal.querySelector(".g-recaptcha") as HTMLElement,
|
||||
"forgotPasswordModal",
|
||||
async () => {
|
||||
await submit();
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
const captchaResponse = CaptchaController.getResponse("forgotPasswordModal");
|
||||
if (!captchaResponse) {
|
||||
Notifications.add("Please complete the captcha");
|
||||
return;
|
||||
}
|
||||
|
||||
const email = (
|
||||
modal.getModal().querySelector("input") as HTMLInputElement
|
||||
).value.trim();
|
||||
|
||||
if (!email) {
|
||||
Notifications.add("Please enter your email address");
|
||||
CaptchaController.reset("forgotPasswordModal");
|
||||
return;
|
||||
}
|
||||
|
||||
const emailSchema = z.string().email();
|
||||
|
||||
const validation = emailSchema.safeParse(email);
|
||||
if (!validation.success) {
|
||||
Notifications.add("Please enter a valid email address");
|
||||
CaptchaController.reset("forgotPasswordModal");
|
||||
return;
|
||||
}
|
||||
|
||||
Loader.show();
|
||||
void Ape.users
|
||||
.forgotPasswordEmail({
|
||||
body: { email, captcha: captchaResponse },
|
||||
})
|
||||
.then((result) => {
|
||||
Loader.hide();
|
||||
if (result.status !== 200) {
|
||||
Notifications.add(
|
||||
"Failed to send password reset email: " + result.body.message,
|
||||
-1
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Notifications.add(result.body.message, 1, { duration: 5 });
|
||||
});
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
function hide(): void {
|
||||
void modal.hide();
|
||||
}
|
||||
|
||||
async function setup(modalEl: HTMLElement): Promise<void> {
|
||||
modalEl.querySelector("button")?.addEventListener("click", async () => {
|
||||
await submit();
|
||||
});
|
||||
}
|
||||
|
||||
const modal = new AnimatedModal({
|
||||
dialogId: "forgotPasswordModal",
|
||||
setup,
|
||||
customEscapeHandler: async (): Promise<void> => {
|
||||
hide();
|
||||
},
|
||||
customWrapperClickHandler: async (): Promise<void> => {
|
||||
hide();
|
||||
},
|
||||
});
|
|
@ -60,7 +60,6 @@ type PopupKey =
|
|||
| "resetProgressCustomTextLong"
|
||||
| "updateCustomTheme"
|
||||
| "deleteCustomTheme"
|
||||
| "forgotPassword"
|
||||
| "devGenerateData";
|
||||
|
||||
const list: Record<PopupKey, SimpleModal | undefined> = {
|
||||
|
@ -86,7 +85,6 @@ const list: Record<PopupKey, SimpleModal | undefined> = {
|
|||
resetProgressCustomTextLong: undefined,
|
||||
updateCustomTheme: undefined,
|
||||
deleteCustomTheme: undefined,
|
||||
forgotPassword: undefined,
|
||||
devGenerateData: undefined,
|
||||
};
|
||||
|
||||
|
@ -1221,46 +1219,6 @@ list.deleteCustomTheme = new SimpleModal({
|
|||
},
|
||||
});
|
||||
|
||||
list.forgotPassword = new SimpleModal({
|
||||
id: "forgotPassword",
|
||||
title: "Forgot password",
|
||||
inputs: [
|
||||
{
|
||||
type: "text",
|
||||
placeholder: "email",
|
||||
initVal: "",
|
||||
},
|
||||
],
|
||||
buttonText: "send",
|
||||
execFn: async (_thisPopup, email): Promise<ExecReturn> => {
|
||||
const result = await Ape.users.forgotPasswordEmail({
|
||||
body: { email: email.trim() },
|
||||
});
|
||||
if (result.status !== 200) {
|
||||
return {
|
||||
status: -1,
|
||||
message: "Failed to send password reset email: " + result.body.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 1,
|
||||
message: result.body.message,
|
||||
notificationOptions: {
|
||||
duration: 8,
|
||||
},
|
||||
};
|
||||
},
|
||||
beforeInitFn: (thisPopup): void => {
|
||||
const inputValue = $(
|
||||
`.pageLogin .login input[name="current-email"]`
|
||||
).val() as string;
|
||||
if (inputValue) {
|
||||
(thisPopup.inputs[0] as TextInput).initVal = inputValue;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
list.devGenerateData = new SimpleModal({
|
||||
id: "devGenerateData",
|
||||
title: "Generate data",
|
||||
|
@ -1365,10 +1323,6 @@ export function showPopup(
|
|||
}
|
||||
|
||||
//todo: move these event handlers to their respective files (either global event files or popup files)
|
||||
$(".pageLogin #forgotPasswordButton").on("click", () => {
|
||||
showPopup("forgotPassword");
|
||||
});
|
||||
|
||||
$(".pageAccountSettings").on("click", "#unlinkDiscordButton", () => {
|
||||
showPopup("unlinkDiscord");
|
||||
});
|
||||
|
|
|
@ -302,6 +302,7 @@ export const ReportUserRequestSchema = z.object({
|
|||
export type ReportUserRequest = z.infer<typeof ReportUserRequestSchema>;
|
||||
|
||||
export const ForgotPasswordEmailRequestSchema = z.object({
|
||||
captcha: z.string(),
|
||||
email: z.string().email(),
|
||||
});
|
||||
export type ForgotPasswordEmailRequest = z.infer<
|
||||
|
|
Loading…
Add table
Reference in a new issue