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