From d0140f4cfbb4aeadf4dd93fe64b178cb95e7b414 Mon Sep 17 00:00:00 2001 From: julius Date: Mon, 22 Dec 2025 11:51:54 +0100 Subject: [PATCH] implement reset password email --- cutt/forgotten_password.html | 111 +++++++++++++++++++++++++++++++++++ cutt/mail.py | 58 ++++++++++++++++++ cutt/main.py | 4 ++ cutt/security.py | 24 ++++---- forgotten_password.py | 2 +- 5 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 cutt/forgotten_password.html create mode 100644 cutt/mail.py diff --git a/cutt/forgotten_password.html b/cutt/forgotten_password.html new file mode 100644 index 0000000..c1e85f7 --- /dev/null +++ b/cutt/forgotten_password.html @@ -0,0 +1,111 @@ + + + + + cutt - set password + + +

+ + + + + + CUTT + + + +

+

Hello USER,

+

click on the following link to set yourself a new password.

+

+ reset password +

+

Cheers,
Julius

+ + diff --git a/cutt/mail.py b/cutt/mail.py new file mode 100644 index 0000000..912b084 --- /dev/null +++ b/cutt/mail.py @@ -0,0 +1,58 @@ +from email.message import EmailMessage +from email.utils import formataddr +import smtplib +import os +import ssl +from dotenv import load_dotenv +from fastapi import Response, status +from sqlmodel import Session, select +from cutt.db import Player, TokenDB, engine +from cutt.security import set_password_token + +P = Player + +load_dotenv() + + +def generate_password_link(user: Player): + with Session(engine) as session: + token = set_password_token(user) + if token: + session.add(TokenDB(token=token)) + session.commit() + return f"https://cutt.0124816.xyz/setpassword?token={token}" + + +def send_forgotten_password_link(email: str): + with Session(engine) as session: + user = session.exec( + select(P).where(P.email == email, P.disabled != True) + ).one_or_none() + if user and user.email: + link = generate_password_link(user) + msg = EmailMessage() + msg["Subject"] = "CUTT - reset password" + msg["From"] = "CUTT - cool ultimate team tool " + msg["To"] = formataddr((user.display_name, user.email)) + msg.set_content( + f"Hello {user.display_name},\nclick on the following link to set yourself a new password.\n\n{link}\n\nCheers,\nJulius" + ) + with open("cutt/forgotten_password.html") as f: + html_body = ( + f.read().replace("USER", user.display_name).replace("LINK", link) + ) + msg.add_alternative(html_body, subtype="html") + context = ssl.create_default_context() + with smtplib.SMTP( + host=os.environ["SMTP_HOST"], + port=int(os.environ["SMTP_PORT"]), + timeout=20, + ) as server: + server.starttls(context=context) + server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"]) + server.send_message(msg) + + return Response( + "a link will be sent to this email, if it belongs to an existing user.", + status_code=status.HTTP_200_OK, + ) diff --git a/cutt/main.py b/cutt/main.py index d1b5a4f..0431b93 100644 --- a/cutt/main.py +++ b/cutt/main.py @@ -10,6 +10,7 @@ from sqlmodel import ( ) from fastapi.middleware.cors import CORSMiddleware from cutt.analysis import analysis_router +from cutt.mail import send_forgotten_password_link from cutt.security import ( get_current_active_user, login_for_access_token, @@ -229,6 +230,9 @@ api_router.include_router( api_router.include_router(team_router, dependencies=[Depends(get_current_active_user)]) api_router.include_router(analysis_router) api_router.add_api_route("/token", endpoint=login_for_access_token, methods=["POST"]) +api_router.add_api_route( + "/forgotten_password", endpoint=send_forgotten_password_link, methods=["POST"] +) api_router.add_api_route("/set_password", endpoint=set_first_password, methods=["POST"]) api_router.add_api_route("/register", endpoint=register, methods=["POST"]) api_router.add_api_route("/logout", endpoint=logout, methods=["POST"]) diff --git a/cutt/security.py b/cutt/security.py index 519f65b..99da870 100644 --- a/cutt/security.py +++ b/cutt/security.py @@ -212,19 +212,17 @@ async def logout(response: Response): return {"message": "Successfully logged out"} -def set_password_token(username: str): - user = get_user(username) - if user: - expire = timedelta(days=30) - token = create_access_token( - data={ - "sub": "set password", - "username": username, - "name": user.display_name, - }, - expires_delta=expire, - ) - return token +def set_password_token(user: Player): + expire = timedelta(days=30) + token = create_access_token( + data={ + "sub": "set password", + "username": user.username, + "name": user.display_name, + }, + expires_delta=expire, + ) + return token def register_token(team_id: int): diff --git a/forgotten_password.py b/forgotten_password.py index eb2b31a..18e4559 100644 --- a/forgotten_password.py +++ b/forgotten_password.py @@ -7,7 +7,7 @@ from cutt.db import Player, TokenDB, engine if len(sys.argv) > 1: with Session(engine) as session: for p in session.exec( - select(Player.username).where(Player.username == sys.argv[1].strip()) + select(Player).where(Player.username == sys.argv[1].strip()) ): print(p) token = set_password_token(p)