From 6f69dde171414f7fd7860e5ca92f1181e2b0d8e5 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Wed, 12 Feb 2025 22:18:57 +0100 Subject: [PATCH 1/3] add password --- app/main.py | 2 +- app/models/users.py | 3 + app/routers/password.py | 99 ++++++++++++++++++++++ app/templates/password_update_success.html | 65 ++++++++++++++ 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 app/routers/password.py create mode 100644 app/templates/password_update_success.html diff --git a/app/main.py b/app/main.py index 0d88ef2..cadd996 100644 --- a/app/main.py +++ b/app/main.py @@ -30,7 +30,7 @@ app.include_router(token.router) app.include_router(mail.router) app.include_router(events.router) app.include_router(tags.router) - +app.include_router(password.router) @app.on_event("startup") async def startup_event(): diff --git a/app/models/users.py b/app/models/users.py index c99e820..6abf94f 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -46,6 +46,9 @@ class UserCreate(BaseModel): firstName: str name: str +class UserForgotPassword(BaseModel): + email: EmailStr + class UserInDB(User): password: str diff --git a/app/routers/password.py b/app/routers/password.py new file mode 100644 index 0000000..d45d22c --- /dev/null +++ b/app/routers/password.py @@ -0,0 +1,99 @@ +from fastapi import APIRouter, HTTPException, status, Request, Form +from fastapi.templating import Jinja2Templates +from ..dependencies import users_token, database, mail +from ..models import users, email +from fastapi.responses import JSONResponse, HTMLResponse +from fastapi_mail import MessageSchema, MessageType, FastMail +import random, os +router = APIRouter() + +# Assurer que le chemin vers "templates" est correct +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates")) + +@router.post("/password/forgot", tags=["password"]) +async def forgot_password(userSingle: users.UserForgotPassword): + if not userSingle.email: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email is required" + ) + + user_repository = users.UserRepository(database=database.database) + user = user_repository.find_one_by({"email": {"$eq": userSingle.email}}) + + if user is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + # Génération d'un token temporaire + reset_token = str(random.randint(100000, 999999)) + key_hashed = users_token.get_password_hash(reset_token) + + email_body = {"key": reset_token, "username": user.username} + email_schema = email.EmailSchema(email=[user.email], body=email_body) + message = MessageSchema( + subject="Password Reset Request", + recipients=email_schema.dict().get("email"), + template_body=email_schema.dict().get("body"), + subtype=MessageType.html, + ) + + fm = FastMail(mail.conf) + await fm.send_message(message, template_name="reset_password.html") + + # Stockage du token temporaire dans Redis avec une expiration + database.connect_redis.setex(user.email, 3600, key_hashed) # Expire dans 1 heure + + return JSONResponse(status_code=status.HTTP_200_OK, content={"message": "Password reset email has been sent"}) + +@router.get("/password/reset", tags=["password"]) +async def reset_password(request: Request, key: str | None = None, email: str | None = None): + if key is None or email is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Parameter key or/and email is empty" + ) + + key_hashed = database.connect_redis.get(email) + + if key_hashed is None or key_hashed.decode() != key: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Key is invalid or expired" + ) + + return templates.TemplateResponse("reset_password.html", {"request": request, "email": email, "key": key}) + +@router.post("/password/update", tags=["password"]) +async def update_password(email: str = Form(...), key: str = Form(...), new_password: str = Form(...), request: Request): + # Vérification du token dans Redis + key_hashed = database.connect_redis.get(email) + + if key_hashed is None or key_hashed.decode() != key: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Key is invalid or expired" + ) + + # Recherche de l'utilisateur dans la base de données + user_repository = users.UserRepository(database=database.database) + user = user_repository.find_one_by({"email": {"$eq": email}}) + + if user is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + # Mise à jour du mot de passe de l'utilisateur + user.password = users_token.get_password_hash(new_password) + user_repository.save(user) + + # Suppression du token temporaire dans Redis + database.connect_redis.delete(email) + + # Renvoyer une réponse HTML après la mise à jour réussie + return templates.TemplateResponse("password_update_success.html", {"request": request, "email": email}) diff --git a/app/templates/password_update_success.html b/app/templates/password_update_success.html new file mode 100644 index 0000000..2a6bb6a --- /dev/null +++ b/app/templates/password_update_success.html @@ -0,0 +1,65 @@ + + + + + + + Mot de Passe Mis à Jour + + + +
+

Votre mot de passe a été mis à jour avec succès

+

Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant utiliser votre nouveau mot de passe pour vous connecter.

+
+ + + + -- 2.47.2 From 672eeea1102f99383bd9f0b8c1ffbf69b3f98599 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Wed, 12 Feb 2025 22:45:59 +0100 Subject: [PATCH 2/3] password forgot / reset / update --- app/routers/password.py | 39 +++++++---- app/templates/forgot_password_email.html | 69 +++++++++++++++++++ app/templates/reset_password.html | 87 ++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 app/templates/forgot_password_email.html create mode 100644 app/templates/reset_password.html diff --git a/app/routers/password.py b/app/routers/password.py index d45d22c..6feb88c 100644 --- a/app/routers/password.py +++ b/app/routers/password.py @@ -3,8 +3,9 @@ from fastapi.templating import Jinja2Templates from ..dependencies import users_token, database, mail from ..models import users, email from fastapi.responses import JSONResponse, HTMLResponse -from fastapi_mail import MessageSchema, MessageType, FastMail -import random, os +from fastapi_mail import MessageSchema, MessageType, FastMail +import random, os + router = APIRouter() # Assurer que le chemin vers "templates" est correct @@ -19,6 +20,7 @@ async def forgot_password(userSingle: users.UserForgotPassword): detail="Email is required" ) + # Recherche de l'utilisateur dans la base de données user_repository = users.UserRepository(database=database.database) user = user_repository.find_one_by({"email": {"$eq": userSingle.email}}) @@ -28,35 +30,45 @@ async def forgot_password(userSingle: users.UserForgotPassword): detail="User not found" ) - # Génération d'un token temporaire + # Génération d'un token temporaire pour réinitialisation reset_token = str(random.randint(100000, 999999)) key_hashed = users_token.get_password_hash(reset_token) - email_body = {"key": reset_token, "username": user.username} - email_schema = email.EmailSchema(email=[user.email], body=email_body) + # Créer le lien de réinitialisation + reset_link = f"https://votresite.com/password/reset?key={reset_token}&email={user.email}" + + # Préparer les données à envoyer au template + email_body = { + "username": user.username, + "reset_link": reset_link + } + + # Créer le message à envoyer message = MessageSchema( subject="Password Reset Request", - recipients=email_schema.dict().get("email"), - template_body=email_schema.dict().get("body"), + recipients=[user.email], + template_body=email_body, subtype=MessageType.html, ) + # Utilisation de FastMail pour envoyer l'email fm = FastMail(mail.conf) - await fm.send_message(message, template_name="reset_password.html") + await fm.send_message(message, template_name="forgot_password_email.html") - # Stockage du token temporaire dans Redis avec une expiration - database.connect_redis.setex(user.email, 3600, key_hashed) # Expire dans 1 heure + # Stockage du token temporaire dans Redis avec une expiration d'1 heure + database.connect_redis.setex(user.email, 3600, key_hashed) return JSONResponse(status_code=status.HTTP_200_OK, content={"message": "Password reset email has been sent"}) @router.get("/password/reset", tags=["password"]) async def reset_password(request: Request, key: str | None = None, email: str | None = None): - if key is None or email is None: + if not key or not email: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Parameter key or/and email is empty" + detail="Parameters 'key' and 'email' are required" ) + # Vérifier que la clé correspond à celle stockée dans Redis key_hashed = database.connect_redis.get(email) if key_hashed is None or key_hashed.decode() != key: @@ -65,6 +77,7 @@ async def reset_password(request: Request, key: str | None = None, email: str | detail="Key is invalid or expired" ) + # Afficher la page HTML de réinitialisation du mot de passe return templates.TemplateResponse("reset_password.html", {"request": request, "email": email, "key": key}) @router.post("/password/update", tags=["password"]) @@ -95,5 +108,5 @@ async def update_password(email: str = Form(...), key: str = Form(...), new_pass # Suppression du token temporaire dans Redis database.connect_redis.delete(email) - # Renvoyer une réponse HTML après la mise à jour réussie + # Afficher un message de succès dans une réponse HTML return templates.TemplateResponse("password_update_success.html", {"request": request, "email": email}) diff --git a/app/templates/forgot_password_email.html b/app/templates/forgot_password_email.html new file mode 100644 index 0000000..9291baf --- /dev/null +++ b/app/templates/forgot_password_email.html @@ -0,0 +1,69 @@ + + + + + + + Réinitialisation de votre mot de passe + + + +
+

Demande de réinitialisation du mot de passe

+

Bonjour {{ username }},

+

Nous avons reçu une demande pour réinitialiser votre mot de passe sur notre site. Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet email.

+

Pour réinitialiser votre mot de passe, cliquez sur le lien ci-dessous :

+

Réinitialiser mon mot de passe

+

Le lien est valable pendant une heure. Si vous ne pouvez pas cliquer dessus, copiez et collez-le dans votre navigateur.

+
+ + + + diff --git a/app/templates/reset_password.html b/app/templates/reset_password.html new file mode 100644 index 0000000..6c1f1c1 --- /dev/null +++ b/app/templates/reset_password.html @@ -0,0 +1,87 @@ + + + + + + Réinitialisation du mot de passe + + + +
+

Réinitialisation du mot de passe

+ +
+ + + +
+ + +
+ + +
+ +
+

Vous avez des questions ? Contactez-nous

+
+
+ + -- 2.47.2 From 9ab9058927402b6fc43b25351dbba3b8afd93a15 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Thu, 13 Feb 2025 20:41:16 +0100 Subject: [PATCH 3/3] fix password --- app/main.py | 2 +- app/routers/password.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index cadd996..e8b578b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from .routers import users, token, mail, events, tags +from .routers import users, token, mail, events, tags, password from .dependencies import user_add import os diff --git a/app/routers/password.py b/app/routers/password.py index 6feb88c..4ef7cb9 100644 --- a/app/routers/password.py +++ b/app/routers/password.py @@ -81,8 +81,7 @@ async def reset_password(request: Request, key: str | None = None, email: str | return templates.TemplateResponse("reset_password.html", {"request": request, "email": email, "key": key}) @router.post("/password/update", tags=["password"]) -async def update_password(email: str = Form(...), key: str = Form(...), new_password: str = Form(...), request: Request): - # Vérification du token dans Redis +async def update_password(request: Request, email: str = Form(...), key: str = Form(...), new_password: str = Form(...)): # Vérification du token dans Redis key_hashed = database.connect_redis.get(email) if key_hashed is None or key_hashed.decode() != key: -- 2.47.2