From 535004bced835e912e529caa71e74db17ba0fb87 Mon Sep 17 00:00:00 2001
From: augmentedpotato
Date: Wed, 15 Apr 2026 00:17:26 -0600
Subject: [PATCH] password reset
---
web/app/forgot-password/page.js | 92 ++++++++++++++++++++++
web/app/login/page.js | 5 ++
web/app/register/page.js | 5 ++
web/app/reset-password/page.js | 132 ++++++++++++++++++++++++++++++++
4 files changed, 234 insertions(+)
create mode 100644 web/app/forgot-password/page.js
create mode 100644 web/app/reset-password/page.js
diff --git a/web/app/forgot-password/page.js b/web/app/forgot-password/page.js
new file mode 100644
index 00000000..0b7adbea
--- /dev/null
+++ b/web/app/forgot-password/page.js
@@ -0,0 +1,92 @@
+"use client";
+
+import dynamic from "next/dynamic";
+import Link from "next/link";
+import { useState } from "react";
+
+function ForgotPasswordPage() {
+ const [usernameOrEmail, setUsernameOrEmail] = useState("");
+ const [message, setMessage] = useState("");
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [submitted, setSubmitted] = useState(false);
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+ setError("");
+ setMessage("");
+ setLoading(true);
+
+ try {
+ const res = await fetch("/api/v1/auth/forgot-password", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ usernameOrEmail }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw new Error(data.message || "Something went wrong. Please try again.");
+ }
+
+ setMessage(data.message || "If an account matches, a reset link has been sent to your email.");
+ setSubmitted(true);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
Forgot Password
+
+ {!submitted ? (
+ <>
+
+ Enter your username or email address and we'll send you a link to reset your password.
+
+
+ {error &&
{error}
}
+
+
+ >
+ ) : (
+
{message}
+ )}
+
+
+ Remember your password?{" "}
+ Log in here
+
+
+ Don't have an account?{" "}
+ Register here
+
+
+
+ );
+}
+
+export default dynamic(() => Promise.resolve(ForgotPasswordPage), {
+ ssr: false,
+});
diff --git a/web/app/login/page.js b/web/app/login/page.js
index 2e7d1fe1..c63d89a6 100644
--- a/web/app/login/page.js
+++ b/web/app/login/page.js
@@ -82,6 +82,11 @@ function LoginPage() {
Don't have an account?{" "}
Register here
+
+
+ Forgot your password?{" "}
+ Reset it here
+
);
diff --git a/web/app/register/page.js b/web/app/register/page.js
index 3b3ba59b..674e4b9e 100644
--- a/web/app/register/page.js
+++ b/web/app/register/page.js
@@ -174,6 +174,11 @@ function RegisterPage() {
Already have an account?{" "}
Log in here
+
+
+ Forgot your password?{" "}
+ Reset it here
+
);
diff --git a/web/app/reset-password/page.js b/web/app/reset-password/page.js
new file mode 100644
index 00000000..2b63b655
--- /dev/null
+++ b/web/app/reset-password/page.js
@@ -0,0 +1,132 @@
+"use client";
+
+import dynamic from "next/dynamic";
+import Link from "next/link";
+import { useState } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+
+function ResetPasswordPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const token = searchParams.get("token") || "";
+
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+
+ if (!token) {
+ return (
+
+
+
Invalid Link
+
+ This password reset link is missing or invalid. Please request a new one.
+
+
+ Request a new reset link
+
+
+
+ );
+ }
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+ setError("");
+
+ if (newPassword !== confirmPassword) {
+ setError("Passwords do not match.");
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const res = await fetch("/api/v1/auth/reset-password", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token, newPassword }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw new Error(data.message || "Failed to reset password. The link may have expired.");
+ }
+
+ setSuccess(true);
+ setTimeout(() => router.push("/login"), 3000);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ if (success) {
+ return (
+
+
+
Password Reset
+
+ Your password has been reset successfully. Redirecting you to login…
+
+
+ Go to login
+
+
+
+ );
+ }
+
+ return (
+
+
+
Reset Password
+
+ {error &&
{error}
}
+
+
+
+
+ Remember your password?{" "}
+ Log in here
+
+
+
+ );
+}
+
+export default dynamic(() => Promise.resolve(ResetPasswordPage), {
+ ssr: false,
+});