correction login process and adding reset password feature, email notification

main
Anaz 2025-01-10 15:46:25 +04:00
parent e3732e1c9a
commit 0ec927bc8a
5 changed files with 158 additions and 15 deletions

6
.env
View File

@ -18,10 +18,10 @@ CELERY_BROKER_URL=redis://localhost:6379/0
LOG_LEVEL=INFO LOG_LEVEL=INFO
# Configuration pour l'envoi d'emails (à remplir si nécessaire) # 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_PORT=587
EMAIL_USERNAME=your-email@example.com EMAIL_USERNAME=noreply@api.mayotte-urgence.com
EMAIL_PASSWORD=your-email-password EMAIL_PASSWORD=Bp@U3VgzrZ@
# Configuration pour le RGPD # Configuration pour le RGPD
GDPR_DELETION_DELAY_DAYS=7 GDPR_DELETION_DELAY_DAYS=7

View File

@ -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 fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy import update, select
from services.auth_service import AuthService from services.auth_service import AuthService
from models.schemas import Token, UserCreate, UserResponse from models.schemas import Token, UserCreate, UserResponse
from config.database import get_db 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() 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") @router.get("/me", response_model=UserResponse, summary="Get current user")
async def read_users_me(current_user: UserResponse = Depends(AuthService.get_current_user)): async def read_users_me(current_user: UserResponse = Depends(AuthService.get_current_user)):
return 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"""
<html>
<body>
<p>You requested a password reset.<br>
Click the link below to reset your password:<br>
<a href="{reset_link}">Redefinir mon mot de passe</a>
</p>
</body>
</html>
"""
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}")

View File

@ -10,12 +10,13 @@ class Settings(BaseSettings):
aws_bucket_name: str = "" aws_bucket_name: str = ""
celery_broker_url: str = "" celery_broker_url: str = ""
log_level: str = "INFO" log_level: str = "INFO"
email_host: str = "" email_host: str = "node267-eu.n0c.com"
email_port: int = 587 email_port: int = 587
email_username: str = "" email_username: str = "noreply@api.mayotte-urgence.com"
email_password: str = "" email_password: str = "Bp@U3VgzrZ@"
gdpr_deletion_delay_days: int = 7 gdpr_deletion_delay_days: int = 7
testing: bool = False testing: bool = False
resetpass_url : str = "https://resetpass.mayotte-urgence.com"
class Config: class Config:
env_file = ".env" env_file = ".env"

View File

@ -9,7 +9,10 @@ from models.schemas import TokenData, UserCreate, UserResponse
from config.database import get_db from config.database import get_db
from models.db import users_table from models.db import users_table
from sqlalchemy import select, update, insert 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 # Configuration pour le hachage des mots de passe
@ -21,6 +24,16 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
class AuthService: class AuthService:
@staticmethod @staticmethod
def verify_password(plain_password: str, hashed_password: str) -> bool: 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) return pwd_context.verify(plain_password, hashed_password)
@staticmethod @staticmethod
@ -31,8 +44,8 @@ class AuthService:
async def authenticate_user(email: str, password: str, db): async def authenticate_user(email: str, password: str, db):
query = select(users_table).where(users_table.c.email == email) query = select(users_table).where(users_table.c.email == email)
result = await db.execute(query) 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: if not user:
return None return None
if not AuthService.verify_password(password, user["hashed_password"]): if not AuthService.verify_password(password, user["hashed_password"]):
@ -40,6 +53,7 @@ class AuthService:
return user return user
@staticmethod @staticmethod
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy() to_encode = data.copy()
@ -62,17 +76,26 @@ class AuthService:
email: str = payload.get("sub") email: str = payload.get("sub")
if email is None: if email is None:
raise credentials_exception raise credentials_exception
token_data = TokenData(email=email)
except JWTError: except JWTError:
raise credentials_exception 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) result = await db.execute(query)
user = result.fetchone() user = result.fetchone()
if user is None: if user is None:
raise credentials_exception 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 @staticmethod
async def create_user(user: UserCreate, db): async def create_user(user: UserCreate, db):
@ -94,13 +117,16 @@ class AuthService:
result = await db.execute(query) result = await db.execute(query)
await db.commit() await db.commit()
user_id = result.inserted_primary_key[0] 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} return {"id": user_id, "email": user.email, "role": role}
except Exception as e: except Exception as e:
await db.rollback() 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)}") raise HTTPException(status_code=500, detail=f"Could not create user: {str(e)}")
@staticmethod @staticmethod
async def admin_required(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)): async def admin_required(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)):
credentials_exception = HTTPException( credentials_exception = HTTPException(
@ -114,8 +140,10 @@ class AuthService:
raise credentials_exception raise credentials_exception
token_data = TokenData(email=email) token_data = TokenData(email=email)
except JWTError: except JWTError:
logger.error("could not decode token", extra={"token": token})
raise credentials_exception raise credentials_exception
query = users_table.select().where(users_table.c.email == token_data.email) query = users_table.select().where(users_table.c.email == token_data.email)
result = await db.execute(query) result = await db.execute(query)
user = result.fetchone() user = result.fetchone()
@ -123,6 +151,7 @@ class AuthService:
raise credentials_exception raise credentials_exception
if user["role"] != "admin": if user["role"] != "admin":
logger.error("admin required", extra={"email": token_data.email})
raise credentials_exception raise credentials_exception
return user return user
@ -190,3 +219,9 @@ class AuthService:
updated_user = result.fetchone() updated_user = result.fetchone()
return UserResponse(**updated_user) 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()

View File

@ -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')