From 6a0f3426b3ffb287d80602254e49e8cd2a32c6e6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Feb 2026 22:14:42 +0000 Subject: [PATCH] recovery form, fix email service --- client/src/Hooks/useRecoveryForm.ts | 12 +++++ client/src/Pages/Auth/Recovery/index.tsx | 44 +++++++++++++++++-- client/src/Validation/recovery.ts | 11 +++++ server/src/routes/settingsRoute.ts | 2 +- .../service/infrastructure/emailService.ts | 4 +- 5 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 client/src/Hooks/useRecoveryForm.ts create mode 100644 client/src/Validation/recovery.ts diff --git a/client/src/Hooks/useRecoveryForm.ts b/client/src/Hooks/useRecoveryForm.ts new file mode 100644 index 000000000..5a3633e45 --- /dev/null +++ b/client/src/Hooks/useRecoveryForm.ts @@ -0,0 +1,12 @@ +import { recoverySchema } from "@/Validation/recovery"; + +export const useRecoveryForm = () => { + const defaults = { + email: "", + }; + + return { + schema: recoverySchema, + defaults, + }; +}; diff --git a/client/src/Pages/Auth/Recovery/index.tsx b/client/src/Pages/Auth/Recovery/index.tsx index 5df9cb1f4..5add53077 100644 --- a/client/src/Pages/Auth/Recovery/index.tsx +++ b/client/src/Pages/Auth/Recovery/index.tsx @@ -1,22 +1,58 @@ import { Button, TextField } from "@/Components/v2/inputs"; import { BaseAuthPage, TextLink } from "@/Components/v2/design-elements"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"; +import { useRecoveryForm } from "@/Hooks/useRecoveryForm"; +import type { RecoveryFormData } from "@/Validation/recovery"; +import { usePost } from "@/Hooks/UseApi"; import { useTranslation } from "react-i18next"; + const ForgotPasswordPage = () => { const { t } = useTranslation(); + const { post, loading } = usePost(); + + const { schema, defaults } = useRecoveryForm(); + + const { control, handleSubmit } = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); + + const onSubmit = async (data: RecoveryFormData) => { + if (loading) return; + + const result = await post("/auth/recovery/request", data); + + if (result?.success) { + // Navigate to Check email page + } + }; + return ( - ( + + )} /> @@ -24,7 +60,7 @@ const ForgotPasswordPage = () => { alignSelf={"center"} text={t("pages.auth.forgotPassword.links.login.text")} linkText={t("pages.auth.forgotPassword.links.login.linkText")} - href={"/login"} + href="/login" /> ); diff --git a/client/src/Validation/recovery.ts b/client/src/Validation/recovery.ts new file mode 100644 index 000000000..356f28738 --- /dev/null +++ b/client/src/Validation/recovery.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const recoverySchema = z.object({ + email: z + .string() + .min(1, "auth.common.inputs.email.errors.empty") + .email("auth.common.inputs.email.errors.invalid") + .transform((val) => val.toLowerCase().trim()), +}); + +export type RecoveryFormData = z.infer; diff --git a/server/src/routes/settingsRoute.ts b/server/src/routes/settingsRoute.ts index c7db1ed3b..c03c17524 100755 --- a/server/src/routes/settingsRoute.ts +++ b/server/src/routes/settingsRoute.ts @@ -13,7 +13,7 @@ class SettingsRoutes { initRoutes() { this.router.get("/", this.settingsController.getAppSettings); - this.router.put("/", isAllowed(["admin", "superadmin"]), this.settingsController.updateAppSettings); + this.router.patch("/", isAllowed(["admin", "superadmin"]), this.settingsController.updateAppSettings); this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.settingsController.sendTestEmail); } diff --git a/server/src/service/infrastructure/emailService.ts b/server/src/service/infrastructure/emailService.ts index 8598f112f..1f46a080a 100755 --- a/server/src/service/infrastructure/emailService.ts +++ b/server/src/service/infrastructure/emailService.ts @@ -29,18 +29,18 @@ class EmailService { this.mjml2html = mjml2html; this.nodemailer = nodemailer; this.logger = logger; - this.init(); this.templateLookup = {}; this.loadTemplate = () => { return () => {}; }; + this.init(); } get serviceName() { return EmailService.SERVICE_NAME; } - init = async () => { + init = () => { this.loadTemplate = (templateName) => { try { const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`);