correction login process and adding reset password feature, email notification
parent
e3732e1c9a
commit
0ec927bc8a
6
.env
6
.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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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')
|
||||
Loading…
Reference in New Issue