mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-12 09:49:39 +02:00
- Add User model with authentication - Create JWT-based auth service - Add login page with admin/admin default credentials - Create admin panel with user management, settings, and site configuration - Move settings to admin section (admin only) - Change search page from multi-site checkboxes to single site selector - Add logout functionality to layout - Protect routes with authentication
307 lines
8.6 KiB
Python
307 lines
8.6 KiB
Python
"""
|
|
Authentication router for login, user management, and admin functions.
|
|
"""
|
|
import logging
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, Header, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app import models
|
|
from app.database import SessionLocal
|
|
from app.services import auth_service
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
|
|
|
|
def get_db():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# Pydantic models
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
token: str
|
|
username: str
|
|
is_admin: bool
|
|
|
|
|
|
class UserResponse(BaseModel):
|
|
id: int
|
|
username: str
|
|
is_admin: bool
|
|
is_active: bool
|
|
created_at: str
|
|
last_login: str | None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class CreateUserRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
is_admin: bool = False
|
|
|
|
|
|
class ChangePasswordRequest(BaseModel):
|
|
current_password: str
|
|
new_password: str
|
|
|
|
|
|
class AdminChangePasswordRequest(BaseModel):
|
|
new_password: str
|
|
|
|
|
|
def get_current_user(
|
|
authorization: Annotated[str | None, Header()] = None,
|
|
db: Session = Depends(get_db),
|
|
) -> models.User:
|
|
"""Dependency to get current authenticated user from JWT token."""
|
|
if not authorization:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Non authentifié",
|
|
)
|
|
|
|
# Extract token from "Bearer <token>" format
|
|
parts = authorization.split()
|
|
if len(parts) != 2 or parts[0].lower() != "bearer":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Format de token invalide",
|
|
)
|
|
|
|
token = parts[1]
|
|
payload = auth_service.decode_token(token)
|
|
|
|
if not payload:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token invalide ou expiré",
|
|
)
|
|
|
|
user = auth_service.get_user_by_id(db, payload["user_id"])
|
|
if not user or not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Utilisateur non trouvé ou désactivé",
|
|
)
|
|
|
|
return user
|
|
|
|
|
|
def get_admin_user(
|
|
current_user: models.User = Depends(get_current_user),
|
|
) -> models.User:
|
|
"""Dependency to require admin privileges."""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Accès administrateur requis",
|
|
)
|
|
return current_user
|
|
|
|
|
|
@router.post("/login", response_model=LoginResponse)
|
|
def login(request: LoginRequest, db: Session = Depends(get_db)):
|
|
"""Authenticate user and return JWT token."""
|
|
user = auth_service.authenticate_user(db, request.username, request.password)
|
|
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Nom d'utilisateur ou mot de passe incorrect",
|
|
)
|
|
|
|
# Update last login
|
|
auth_service.update_last_login(db, user)
|
|
|
|
# Create token
|
|
token = auth_service.create_token(user.id, user.username, user.is_admin)
|
|
|
|
logger.info(f"User '{user.username}' logged in successfully")
|
|
|
|
return LoginResponse(
|
|
token=token,
|
|
username=user.username,
|
|
is_admin=user.is_admin,
|
|
)
|
|
|
|
|
|
@router.get("/me", response_model=UserResponse)
|
|
def get_current_user_info(current_user: models.User = Depends(get_current_user)):
|
|
"""Get current user information."""
|
|
return UserResponse(
|
|
id=current_user.id,
|
|
username=current_user.username,
|
|
is_admin=current_user.is_admin,
|
|
is_active=current_user.is_active,
|
|
created_at=current_user.created_at.isoformat() if current_user.created_at else "",
|
|
last_login=current_user.last_login.isoformat() if current_user.last_login else None,
|
|
)
|
|
|
|
|
|
@router.post("/change-password")
|
|
def change_password(
|
|
request: ChangePasswordRequest,
|
|
current_user: models.User = Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Change current user's password."""
|
|
# Verify current password
|
|
if not auth_service.verify_password(request.current_password, current_user.password_hash):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Mot de passe actuel incorrect",
|
|
)
|
|
|
|
# Update password
|
|
auth_service.update_password(db, current_user, request.new_password)
|
|
|
|
logger.info(f"User '{current_user.username}' changed password")
|
|
|
|
return {"message": "Mot de passe modifié avec succès"}
|
|
|
|
|
|
# Admin endpoints
|
|
@router.get("/users", response_model=list[UserResponse])
|
|
def list_users(
|
|
admin_user: models.User = Depends(get_admin_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List all users (admin only)."""
|
|
users = auth_service.get_all_users(db)
|
|
return [
|
|
UserResponse(
|
|
id=u.id,
|
|
username=u.username,
|
|
is_admin=u.is_admin,
|
|
is_active=u.is_active,
|
|
created_at=u.created_at.isoformat() if u.created_at else "",
|
|
last_login=u.last_login.isoformat() if u.last_login else None,
|
|
)
|
|
for u in users
|
|
]
|
|
|
|
|
|
@router.post("/users", response_model=UserResponse)
|
|
def create_user(
|
|
request: CreateUserRequest,
|
|
admin_user: models.User = Depends(get_admin_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Create a new user (admin only)."""
|
|
# Check if username already exists
|
|
existing = auth_service.get_user_by_username(db, request.username)
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Ce nom d'utilisateur existe déjà",
|
|
)
|
|
|
|
user = auth_service.create_user(db, request.username, request.password, request.is_admin)
|
|
|
|
logger.info(f"Admin '{admin_user.username}' created user '{user.username}'")
|
|
|
|
return UserResponse(
|
|
id=user.id,
|
|
username=user.username,
|
|
is_admin=user.is_admin,
|
|
is_active=user.is_active,
|
|
created_at=user.created_at.isoformat() if user.created_at else "",
|
|
last_login=None,
|
|
)
|
|
|
|
|
|
@router.delete("/users/{user_id}")
|
|
def delete_user(
|
|
user_id: int,
|
|
admin_user: models.User = Depends(get_admin_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Delete a user (admin only)."""
|
|
# Prevent self-deletion
|
|
if user_id == admin_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Vous ne pouvez pas supprimer votre propre compte",
|
|
)
|
|
|
|
success = auth_service.delete_user(db, user_id)
|
|
if not success:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Utilisateur non trouvé",
|
|
)
|
|
|
|
logger.info(f"Admin '{admin_user.username}' deleted user ID {user_id}")
|
|
|
|
return {"message": "Utilisateur supprimé"}
|
|
|
|
|
|
@router.put("/users/{user_id}/toggle-active", response_model=UserResponse)
|
|
def toggle_user_active(
|
|
user_id: int,
|
|
admin_user: models.User = Depends(get_admin_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Toggle user active status (admin only)."""
|
|
# Prevent self-deactivation
|
|
if user_id == admin_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Vous ne pouvez pas désactiver votre propre compte",
|
|
)
|
|
|
|
user = auth_service.toggle_user_active(db, user_id)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Utilisateur non trouvé",
|
|
)
|
|
|
|
logger.info(f"Admin '{admin_user.username}' toggled active status for user '{user.username}'")
|
|
|
|
return UserResponse(
|
|
id=user.id,
|
|
username=user.username,
|
|
is_admin=user.is_admin,
|
|
is_active=user.is_active,
|
|
created_at=user.created_at.isoformat() if user.created_at else "",
|
|
last_login=user.last_login.isoformat() if user.last_login else None,
|
|
)
|
|
|
|
|
|
@router.put("/users/{user_id}/password")
|
|
def admin_change_user_password(
|
|
user_id: int,
|
|
request: AdminChangePasswordRequest,
|
|
admin_user: models.User = Depends(get_admin_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Change a user's password (admin only)."""
|
|
user = auth_service.get_user_by_id(db, user_id)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Utilisateur non trouvé",
|
|
)
|
|
|
|
auth_service.update_password(db, user, request.new_password)
|
|
|
|
logger.info(f"Admin '{admin_user.username}' changed password for user '{user.username}'")
|
|
|
|
return {"message": "Mot de passe modifié avec succès"}
|