From fc609401c3f82f2090db0bdb9d212dc0a109d7ce Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Thu, 26 Feb 2026 09:40:57 +0000 Subject: [PATCH] feat: add MFA disable (self-service + admin reset) endpoints Co-Authored-By: Claude Opus 4.6 --- backend/app/api/admin.py | 28 ++++++++++++++++++++++++++++ backend/app/api/auth.py | 13 +++++++++++++ backend/app/services/auth_service.py | 16 ++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 0d50a0f..170ab01 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -170,6 +170,34 @@ def update_user( return user +@router.delete("/users/{user_id}/mfa") +def admin_reset_mfa( + user_id: int, + request: Request, + admin: User = Depends(require_admin), + db: Session = Depends(get_db), +): + """Admin: reset MFA on a user's account.""" + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + user.mfa_secret = None + user.mfa_enabled = False + db.commit() + + log_action( + db, + user_id=admin.id, + action="mfa_reset", + entity_type="user", + entity_id=user.id, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent"), + ) + return {"detail": "MFA reset"} + + # --------------------------------------------------------------------------- # Invitations # --------------------------------------------------------------------------- diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 20e4d9a..c48fea3 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -13,6 +13,7 @@ from app.models.user import User from app.schemas.auth import ( ChangePasswordRequest, LoginRequest, + MFADisableRequest, MFASetupResponse, MFAVerifyRequest, RefreshRequest, @@ -25,6 +26,7 @@ from app.services.auth_service import ( authenticate_user, change_password, create_tokens, + disable_mfa, refresh_access_token, register_user, revoke_refresh_token, @@ -139,6 +141,17 @@ def mfa_verify( return {"detail": "MFA enabled"} +@router.delete("/mfa") +def mfa_disable( + data: MFADisableRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """Disable MFA on the authenticated user's account (requires password).""" + disable_mfa(db, current_user, data.password) + return {"detail": "MFA disabled"} + + @router.post("/change-password") def change_password_endpoint( data: ChangePasswordRequest, diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 84db42d..55e6f33 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -319,3 +319,19 @@ def update_profile( db.commit() db.refresh(user) return user + + +# --------------------------------------------------------------------------- +# MFA disable +# --------------------------------------------------------------------------- + +def disable_mfa(db: Session, user: User, password: str) -> None: + """Disable MFA on the user's account after verifying their password.""" + if not verify_password(password, user.password_hash): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Incorrect password", + ) + user.mfa_secret = None + user.mfa_enabled = False + db.commit()