diff --git a/.env b/.env index 5cb319b..b4e0bf0 100644 --- a/.env +++ b/.env @@ -18,10 +18,10 @@ CELERY_BROKER_URL=redis://localhost:6379/0 LOG_LEVEL=INFO # Configuration pour l'envoi d'emails (à remplir si nécessaire) -EMAIL_HOST=smtp.example.com +EMAIL_HOST=node267-eu.n0c.com EMAIL_PORT=587 -EMAIL_USERNAME=your-email@example.com -EMAIL_PASSWORD=your-email-password +EMAIL_USERNAME=noreply@api.mayotte-urgence.com +EMAIL_PASSWORD=Bp@U3VgzrZ@ # Configuration pour le RGPD GDPR_DELETION_DELAY_DAYS=7 diff --git a/api/v1/auth.py b/api/v1/auth.py index 42373e4..531e268 100644 --- a/api/v1/auth.py +++ b/api/v1/auth.py @@ -1,9 +1,20 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from datetime import datetime, timedelta +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from fastapi import APIRouter, Depends, HTTPException, status, Body +from fastapi.responses import JSONResponse from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy import update, select from services.auth_service import AuthService from models.schemas import Token, UserCreate, UserResponse from config.database import get_db -from utils.security import verify_password, get_password_hash +from models.db import users_table +from config.settings import settings +from sqlalchemy.ext.asyncio import AsyncSession +from jose import jwt, JWTError +from smtplib import SMTP +from utils.logging import logger +from utils.security import verify_password, get_password_hash, pwd_context router = APIRouter() @@ -33,3 +44,86 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( @router.get("/me", response_model=UserResponse, summary="Get current user") async def read_users_me(current_user: UserResponse = Depends(AuthService.get_current_user)): return current_user + +@router.post("/reset-password") +async def reset_password(token: str = Body(...), new_password: str = Body(...), db: AsyncSession = Depends(get_db)): + try: + payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) + email = payload.get("sub") + if email is None: + raise HTTPException(status_code=400, detail="Invalid token") + except JWTError: + raise HTTPException(status_code=400, detail="Invalid or expired token") + + # Hash the new password + hashed_password = pwd_context.hash(new_password) + + # Update the user's password + query = ( + update(users_table) + .where(users_table.c.email == email) + .values(hashed_password=hashed_password) + .execution_options(synchronize_session="fetch") + ) + await db.execute(query) + await db.commit() + + return JSONResponse(content={"message": "Password updated successfully."}) + +@router.post("/password-reset-request") +async def password_reset_request(email: str, db: AsyncSession = Depends(get_db)): + query = select(users_table).where(users_table.c.email == email) + result = await db.execute(query) + user = result.fetchone() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Générer un token JWT + reset_token = jwt.encode( + {"sub": email, "exp": datetime.utcnow() + timedelta(hours=1)}, + settings.secret_key, + algorithm=settings.algorithm, + ) + + reset_link = f"{settings.resetpass_url}/?token={reset_token}" + + # Envoyer le lien par email + try: + subject = "mot de passe perdu" + sender_email = settings.email_username + receiver_email = email + logger.info(f"sender {settings.email_username} receiver {email}") + + message = MIMEMultipart("alternative") + message["Subject"] = subject + message["From"] = sender_email + message["To"] = receiver_email + + text = f"You requested a password reset. Click the link below to reset your password:\n{reset_link}" + html = f""" + + +

You requested a password reset.
+ Click the link below to reset your password:
+ Redefinir mon mot de passe +

+ + + """ + + part1 = MIMEText(text, "plain") + part2 = MIMEText(html, "html") + message.attach(part1) + message.attach(part2) + + with SMTP(settings.email_host, settings.email_port) as server: + server.starttls() + server.login(settings.email_username, settings.email_password) + server.sendmail(sender_email, receiver_email, message.as_string()) + + logger.info(f"Password reset email sent to {email}") + return JSONResponse(content={"message": "Password reset link sent via email."}) + + except Exception as e: + logger.error(f"Failed to send password reset email to {email}: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to send password reset email. sender {sender_email} receiver {receiver_email}") diff --git a/config/settings.py b/config/settings.py index e8c2f96..299127c 100644 --- a/config/settings.py +++ b/config/settings.py @@ -10,12 +10,13 @@ class Settings(BaseSettings): aws_bucket_name: str = "" celery_broker_url: str = "" log_level: str = "INFO" - email_host: str = "" + email_host: str = "node267-eu.n0c.com" email_port: int = 587 - email_username: str = "" - email_password: str = "" + email_username: str = "noreply@api.mayotte-urgence.com" + email_password: str = "Bp@U3VgzrZ@" gdpr_deletion_delay_days: int = 7 testing: bool = False + resetpass_url : str = "https://resetpass.mayotte-urgence.com" class Config: env_file = ".env" diff --git a/services/auth_service.py b/services/auth_service.py index 9b9d864..7e59887 100644 --- a/services/auth_service.py +++ b/services/auth_service.py @@ -9,7 +9,10 @@ from models.schemas import TokenData, UserCreate, UserResponse from config.database import get_db from models.db import users_table from sqlalchemy import select, update, insert -from sqlalchemy.ext.asyncio import AsyncSession # Import ajouté ici +from sqlalchemy.ext.asyncio import AsyncSession +from utils.logging import logger + +logger.info("Test log message") # Configuration pour le hachage des mots de passe @@ -21,6 +24,16 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") class AuthService: @staticmethod def verify_password(plain_password: str, hashed_password: str) -> bool: + passEncr = pwd_context.hash(plain_password) + passEncrTest123Azerty = pwd_context.hash("123Azerty") + logger.info("password 123Azerty encrypted : " +passEncrTest123Azerty) + passEncrTestAzerty = pwd_context.hash("Azerty") + logger.info("password Azerty encrypted : " +passEncrTestAzerty) + logger.info("password en clair : " +plain_password) + logger.info("password encrypted : " +passEncr) + logger.info("password hashed : "+ hashed_password) + logger.info("verification plain_password & hashed_password : "+ str(pwd_context.verify(plain_password, hashed_password))) + logger.info("verification plain_password & 123Azerty encrypted : "+ str(pwd_context.verify(plain_password, passEncrTest123Azerty))) return pwd_context.verify(plain_password, hashed_password) @staticmethod @@ -31,15 +44,16 @@ class AuthService: async def authenticate_user(email: str, password: str, db): query = select(users_table).where(users_table.c.email == email) result = await db.execute(query) - user = result.fetchone() - + user = result.mappings().fetchone() # Récupère un dictionnaire plutôt qu'un tuple + logger.info("user authenticated", extra={"email": email}) if not user: - return None + return None if not AuthService.verify_password(password, user["hashed_password"]): return None return user + @staticmethod def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: to_encode = data.copy() @@ -62,17 +76,26 @@ class AuthService: email: str = payload.get("sub") if email is None: raise credentials_exception - token_data = TokenData(email=email) except JWTError: raise credentials_exception - query = select(users_table).where(users_table.c.email == token_data.email) + query = select(users_table).where(users_table.c.email == email) result = await db.execute(query) user = result.fetchone() if user is None: raise credentials_exception - return UserResponse(**user) + # Préparez la réponse avec tous les champs requis + return { + "id": user["id"], + "email": user["email"], + "full_name": user["full_name"], + "phone": user["phone"], + "date_of_birth": user["date_of_birth"].isoformat(), + "role": user["role"], + "is_active": not user["is_blocked"], + "is_banned": user["is_deleted"], + } @staticmethod async def create_user(user: UserCreate, db): @@ -94,10 +117,13 @@ class AuthService: result = await db.execute(query) await db.commit() user_id = result.inserted_primary_key[0] + logger.info("user created", extra={"user_id": user_id, "email": user.email, "role": role}) return {"id": user_id, "email": user.email, "role": role} except Exception as e: await db.rollback() + logger.error("could not create user", extra={"error": str(e)}) raise HTTPException(status_code=500, detail=f"Could not create user: {str(e)}") + @@ -114,7 +140,9 @@ class AuthService: raise credentials_exception token_data = TokenData(email=email) except JWTError: + logger.error("could not decode token", extra={"token": token}) raise credentials_exception + query = users_table.select().where(users_table.c.email == token_data.email) result = await db.execute(query) @@ -123,6 +151,7 @@ class AuthService: raise credentials_exception if user["role"] != "admin": + logger.error("admin required", extra={"email": token_data.email}) raise credentials_exception return user @@ -190,3 +219,9 @@ class AuthService: updated_user = result.fetchone() return UserResponse(**updated_user) + + @staticmethod + async def get_user_by_email(email: str, db: AsyncSession): + query = select(users_table).where(users_table.c.email == email) + result = await db.execute(query) + return result.fetchone() \ No newline at end of file diff --git a/utils/logging.py b/utils/logging.py index e69de29..9985ffa 100644 --- a/utils/logging.py +++ b/utils/logging.py @@ -0,0 +1,13 @@ +import logging +import os + +# Configuration du logger pour la console +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler() # Affiche les logs dans la console + ] +) + +logger = logging.getLogger('ApplicationLogger') \ No newline at end of file