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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,15 +44,16 @@ 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"]):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
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,10 +117,13 @@ 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)}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -114,7 +140,9 @@ 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)
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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