from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, status, Request, Form from fastapi.responses import RedirectResponse from sqlalchemy.orm import Session from loguru import logger from ..db import get_db from ..models import User from ..schemas import UserCreate, UserResponse, LoginRequest, Token from ..auth import ( get_password_hash, verify_password, create_access_token, get_current_user, get_current_active_admin, get_client_ip, get_current_session_token ) from ..config import settings, utcnow from ..logging_service import LoggingService from ..services.security_service import SecurityService router = APIRouter() @router.post("/api/auth/login", response_model=Token) def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_db)): """ Authenticate user and return JWT token with session management """ # Get client info client_ip = get_client_ip(request) user_agent = request.headers.get("user-agent", "")[:500] # Check if IP is blocked is_blocked, block_reason = SecurityService.is_ip_blocked(db, client_ip) if is_blocked: logger.warning(f"Blocked IP {client_ip} attempted login: {block_reason}") SecurityService.record_login_attempt( db, client_ip, login_data.username, False, "IP blocked", user_agent ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Access denied: {block_reason}" ) # Check for brute force should_block, reason = SecurityService.check_brute_force(db, client_ip, login_data.username) if should_block: block_duration = SecurityService.get_setting_int(db, "ip_block_duration_minutes", 60) SecurityService.block_ip(db, client_ip, reason, "system", block_duration) SecurityService.record_login_attempt( db, client_ip, login_data.username, False, "Brute force detected", user_agent ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Too many failed attempts. IP blocked temporarily." ) # Find user user = db.query(User).filter(User.username == login_data.username).first() if not user: SecurityService.record_login_attempt( db, client_ip, login_data.username, False, "User not found", user_agent ) LoggingService.log_auth( db=db, action="login_failed", username=login_data.username, ip_address=client_ip, success=False, error_message="User not found", user_agent=user_agent ) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) # Check if account is locked is_locked, locked_until = SecurityService.is_account_locked(user) if is_locked: SecurityService.record_login_attempt( db, client_ip, login_data.username, False, "Account locked", user_agent ) LoggingService.log_auth( db=db, action="login_blocked", username=login_data.username, ip_address=client_ip, success=False, error_message="Account locked", user_agent=user_agent, user_id=user.id ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Account is locked until {locked_until.strftime('%H:%M:%S')}" ) # Verify password if not verify_password(login_data.password, user.hashed_password): SecurityService.record_login_attempt( db, client_ip, login_data.username, False, "Wrong password", user_agent ) # Increment failed attempts was_locked = SecurityService.increment_failed_attempts(db, user) LoggingService.log_auth( db=db, action="login_failed", username=login_data.username, ip_address=client_ip, success=False, error_message="Incorrect password" + (" - account locked" if was_locked else ""), user_agent=user_agent, user_id=user.id ) if was_locked: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Account locked due to too many failed attempts" ) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) # Check if user is active if not user.is_active: SecurityService.record_login_attempt( db, client_ip, login_data.username, False, "Account disabled", user_agent ) LoggingService.log_auth( db=db, action="login_blocked", username=login_data.username, ip_address=client_ip, success=False, error_message="Account disabled", user_agent=user_agent, user_id=user.id ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled" ) # SUCCESS - Reset failed attempts SecurityService.reset_failed_attempts(db, user) # Create session session, was_other_terminated = SecurityService.create_session( db, user, client_ip, user_agent ) # Update last login user.last_login = utcnow() db.commit() # Create access token with session binding session_timeout = SecurityService.get_setting_int(db, "session_timeout_minutes", 60) access_token_expires = timedelta(minutes=session_timeout) access_token = create_access_token( data={"sub": user.username}, session_token=session.session_token, expires_delta=access_token_expires ) # Record successful login SecurityService.record_login_attempt( db, client_ip, login_data.username, True, None, user_agent ) # Log successful login LoggingService.log_auth( db=db, action="login", username=user.username, ip_address=client_ip, success=True, user_agent=user_agent, user_id=user.id, details={"session_id": session.id, "other_session_terminated": was_other_terminated} ) logger.info(f"User {user.username} logged in from {client_ip}" + (" (other session terminated)" if was_other_terminated else "")) return { "access_token": access_token, "token_type": "bearer", "session_id": session.id, "other_session_terminated": was_other_terminated } @router.post("/api/auth/refresh", response_model=Token) def refresh_token( current_user: User = Depends(get_current_user), session_token: str = Depends(get_current_session_token), db: Session = Depends(get_db) ): """Refresh JWT access token without re-login (extends session)""" session_timeout = SecurityService.get_setting_int(db, "session_timeout_minutes", 60) access_token_expires = timedelta(minutes=session_timeout) new_token = create_access_token( data={"sub": current_user.username}, session_token=session_token, expires_delta=access_token_expires ) return { "access_token": new_token, "token_type": "bearer" } @router.post("/api/auth/logout") def logout( request: Request, current_user: User = Depends(get_current_user), session_token: str = Depends(get_current_session_token), db: Session = Depends(get_db) ): """Logout user and terminate current session only""" client_ip = get_client_ip(request) # Terminate only current session (allow multiple sessions) if session_token: SecurityService.terminate_session(db, session_token) LoggingService.log_auth( db=db, action="logout", username=current_user.username, ip_address=client_ip, success=True, user_id=current_user.id ) logger.info(f"User {current_user.username} logged out") return {"message": "Successfully logged out"} @router.get("/api/auth/me") def get_current_user_info(current_user: User = Depends(get_current_user)): """Get current user information with permissions""" permissions = {} if current_user.role: permissions = current_user.role.permissions or {} elif current_user.is_admin: # Admin has all permissions (new resource:action format) from ..services.security_service import SecurityService all_perms = SecurityService.get_all_permissions_v2() for category, perms in all_perms.items(): for perm in perms: permissions[perm] = True return { "id": current_user.id, "username": current_user.username, "email": current_user.email, "full_name": current_user.full_name, "is_admin": current_user.is_admin, "is_active": current_user.is_active, "role": { "id": current_user.role.id if current_user.role else None, "name": current_user.role.name if current_user.role else ("admin" if current_user.is_admin else "user"), "display_name": current_user.role.display_name if current_user.role else ("Administrator" if current_user.is_admin else "User") }, "permissions": permissions, "require_password_change": current_user.require_password_change, "last_login": current_user.last_login.isoformat() if current_user.last_login else None, "created_at": current_user.created_at.isoformat() if current_user.created_at else None } @router.get("/api/auth/sessions") def get_my_sessions(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): """Get current user's active sessions""" sessions = SecurityService.get_user_sessions(db, current_user.id) return [{ "id": s.id, "ip_address": s.ip_address, "device_info": s.device_info, "created_at": s.created_at.isoformat() if s.created_at else None, "last_activity": s.last_activity.isoformat() if s.last_activity else None, "expires_at": s.expires_at.isoformat() if s.expires_at else None } for s in sessions] @router.post("/api/auth/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED) def register_user( user_data: UserCreate, request: Request, db: Session = Depends(get_db), current_admin: User = Depends(get_current_active_admin) ): """ Register a new user (admin only) """ client_ip = get_client_ip(request) # Check if username already exists if db.query(User).filter(User.username == user_data.username).first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Username already registered" ) # Check if email already exists if db.query(User).filter(User.email == user_data.email).first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" ) # Validate password lang = request.cookies.get("language", "en") is_valid, errors = SecurityService.validate_password(db, user_data.password, lang) if not is_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="; ".join(errors) ) # Create new user hashed_password = get_password_hash(user_data.password) new_user = User( username=user_data.username, email=user_data.email, full_name=user_data.full_name, hashed_password=hashed_password, is_active=True, is_admin=False, last_password_change=utcnow() ) db.add(new_user) db.commit() db.refresh(new_user) LoggingService.log_auth( db=db, action="user_created", username=current_admin.username, ip_address=client_ip, success=True, user_id=current_admin.id, details={"new_user": new_user.username} ) logger.info(f"New user registered: {new_user.username} by admin {current_admin.username}") return new_user @router.get("/api/users", response_model=list[UserResponse]) def list_users( db: Session = Depends(get_db), current_admin: User = Depends(get_current_active_admin) ): """ List all users (admin only) """ users = db.query(User).all() return users @router.delete("/api/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_user( user_id: int, request: Request, db: Session = Depends(get_db), current_admin: User = Depends(get_current_active_admin) ): """ Delete a user (admin only) """ client_ip = get_client_ip(request) user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) # Prevent deleting yourself if user.id == current_admin.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete your own account" ) username = user.username # Terminate sessions before deletion SecurityService.terminate_all_user_sessions(db, user_id) db.delete(user) db.commit() LoggingService.log_auth( db=db, action="user_deleted", username=current_admin.username, ip_address=client_ip, success=True, user_id=current_admin.id, details={"deleted_user": username} ) logger.info(f"User {username} deleted by admin {current_admin.username}") return None @router.post("/api/auth/change-password") # Note: no explicit CSRF token needed here. # This endpoint is protected by JWT Bearer token (Authorization header). # Browsers cannot automatically include the Authorization header in cross-site requests, # so CSRF attacks are not possible — the attacker cannot obtain or inject the token. def change_password( old_password: str = Form(...), new_password: str = Form(...), request: Request = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Change user password """ client_ip = get_client_ip(request) if request else "unknown" # Verify old password if not verify_password(old_password, current_user.hashed_password): LoggingService.log_auth( db=db, action="password_change_failed", username=current_user.username, ip_address=client_ip, success=False, error_message="Incorrect current password", user_id=current_user.id ) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect password" ) # Validate new password lang = request.cookies.get("language", "en") is_valid, errors = SecurityService.validate_password(db, new_password, lang) if not is_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="; ".join(errors) ) # Update password current_user.hashed_password = get_password_hash(new_password) current_user.last_password_change = utcnow() current_user.require_password_change = False db.commit() LoggingService.log_auth( db=db, action="password_changed", username=current_user.username, ip_address=client_ip, success=True, user_id=current_user.id ) logger.info(f"User {current_user.username} changed password") return {"message": "Password changed successfully"}