From 402213697052b853e42112db4623d890ba6ff637 Mon Sep 17 00:00:00 2001
From: julius
Date: Tue, 23 Dec 2025 08:53:46 +0100
Subject: [PATCH] forgotten password routine
---
cutt/mail.py | 20 +++++++---
frontend/src/CUTT.tsx | 2 +
frontend/src/Login.tsx | 87 ++++++++++++++++++++++++++++++++++++++++--
pyproject.toml | 1 +
4 files changed, 101 insertions(+), 9 deletions(-)
diff --git a/cutt/mail.py b/cutt/mail.py
index 912b084..10a4bda 100644
--- a/cutt/mail.py
+++ b/cutt/mail.py
@@ -1,10 +1,13 @@
from email.message import EmailMessage
from email.utils import formataddr
+from random import random
import smtplib
import os
import ssl
+from time import sleep
from dotenv import load_dotenv
-from fastapi import Response, status
+from fastapi.responses import PlainTextResponse
+from pydantic import BaseModel
from sqlmodel import Session, select
from cutt.db import Player, TokenDB, engine
from cutt.security import set_password_token
@@ -23,10 +26,14 @@ def generate_password_link(user: Player):
return f"https://cutt.0124816.xyz/setpassword?token={token}"
-def send_forgotten_password_link(email: str):
+class EmailRequest(BaseModel):
+ email: str
+
+
+def send_forgotten_password_link(email: EmailRequest):
with Session(engine) as session:
user = session.exec(
- select(P).where(P.email == email, P.disabled != True)
+ select(P).where(P.email == email.email, P.disabled != True)
).one_or_none()
if user and user.email:
link = generate_password_link(user)
@@ -51,8 +58,9 @@ def send_forgotten_password_link(email: str):
server.starttls(context=context)
server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
server.send_message(msg)
+ else:
+ sleep(random())
- return Response(
- "a link will be sent to this email, if it belongs to an existing user.",
- status_code=status.HTTP_200_OK,
+ return PlainTextResponse(
+ "a link will be sent to this email, if it belongs to an existing user."
)
diff --git a/frontend/src/CUTT.tsx b/frontend/src/CUTT.tsx
index 0d962c4..e5ab79f 100644
--- a/frontend/src/CUTT.tsx
+++ b/frontend/src/CUTT.tsx
@@ -9,6 +9,7 @@ import { GraphComponent } from "./Network";
import MVPChart from "./MVPChart";
import { SetPassword } from "./SetPassword";
import { Register } from "./Register";
+import { ForgotPassword } from "./Login";
const Maintenance = () => {
return (
@@ -28,6 +29,7 @@ function App() {
} />
} />
+ } />
{
onLogin(user);
}
- function handleSubmit(e: React.FormEvent) {
+ function handleSubmit(e: FormEvent) {
e.preventDefault();
doLogin();
}
@@ -86,6 +86,9 @@ export const Login = ({ onLogin }: LoginProps) => {
}}
/>
+
+ forgot password?
+
{error && {error}
}
@@ -102,3 +105,81 @@ export const Login = ({ onLogin }: LoginProps) => {
);
};
+
+export const ForgotPassword = () => {
+ const [email, setEmail] = useState("");
+ const [error, setError] = useState("");
+
+ async function handleSubmit(e: FormEvent) {
+ e.preventDefault();
+ const req = new Request(`${baseUrl}api/forgotten_password`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email: decodeURI(email) }),
+ });
+ let resp: Response;
+ try {
+ resp = await fetch(req);
+ } catch (e) {
+ throw new Error(`request failed: ${e}`);
+ }
+ if (resp.ok) {
+ const content = await resp.text();
+ if (content) setError(content);
+ }
+
+ if (!resp.ok) {
+ const content = await resp.text();
+ if (content) setError(content);
+ else setError("unauthorized");
+ throw new Error("Unauthorized");
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
Forgot password
+
+
+
+ );
+};
diff --git a/pyproject.toml b/pyproject.toml
index cdacec5..7f44352 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,4 +21,5 @@ dependencies = [
[dependency-groups]
dev = [
"pyqt6>=6.10.1",
+ "python-dotenv>=1.2.1",
]