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
# 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

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 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"""
<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 = ""
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"

View File

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

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