Add comprehensive backend features and mobile UI improvements

Backend:
- Add 2FA authentication with TOTP support
- Add API keys management system
- Add audit logging for security events
- Add file upload/management system
- Add notifications system with preferences
- Add session management
- Add webhooks integration
- Add analytics endpoints
- Add export functionality
- Add password policy enforcement
- Add new database migrations for core tables

Frontend:
- Add module position system (top/bottom sidebar sections)
- Add search and notifications module configuration tabs
- Add mobile logo replacing hamburger menu
- Center page title absolutely when no tabs present
- Align sidebar footer toggles with navigation items
- Add lighter icon color in dark theme for mobile
- Add API keys management page
- Add notifications page with context
- Add admin analytics and audit logs pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-17 22:27:32 +01:00
parent f698aa4d51
commit 8c4a555b88
76 changed files with 9751 additions and 323 deletions

View File

@@ -0,0 +1,361 @@
"""Two-factor authentication (2FA) endpoints."""
import io
import secrets
import base64
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
import pyotp
import qrcode
from app.dependencies import get_db, get_current_user
from app.models.user import User
from app import crud
from app.config import settings
router = APIRouter()
class TOTPSetupResponse(BaseModel):
"""Response for TOTP setup initiation."""
secret: str
uri: str
qr_code: str # Base64 encoded QR code image
class TOTPVerifyRequest(BaseModel):
"""Request to verify TOTP code."""
code: str = Field(..., min_length=6, max_length=6)
class TOTPDisableRequest(BaseModel):
"""Request to disable TOTP."""
password: str
code: str = Field(..., min_length=6, max_length=6)
class BackupCodesResponse(BaseModel):
"""Response with backup codes."""
backup_codes: list[str]
def get_client_ip(request: Request) -> str:
"""Extract client IP from request."""
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
return request.client.host if request.client else "unknown"
def generate_backup_codes(count: int = 10) -> list[str]:
"""Generate backup codes for 2FA recovery."""
return [secrets.token_hex(4).upper() for _ in range(count)]
def generate_qr_code(uri: str) -> str:
"""Generate QR code as base64 encoded PNG."""
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = io.BytesIO()
img.save(buffer, format="PNG")
buffer.seek(0)
return base64.b64encode(buffer.getvalue()).decode()
@router.get("/status")
def get_2fa_status(
current_user: User = Depends(get_current_user),
) -> Any:
"""
Get 2FA status for the current user.
"""
return {
"enabled": current_user.totp_enabled,
"has_backup_codes": bool(current_user.backup_codes)
}
@router.post("/setup", response_model=TOTPSetupResponse)
def setup_2fa(
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> Any:
"""
Initiate 2FA setup by generating a new TOTP secret.
Returns the secret, URI, and QR code for authenticator app setup.
The user must verify the code before 2FA is enabled.
"""
if current_user.totp_enabled:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="2FA is already enabled"
)
# Generate new secret
secret = pyotp.random_base32()
# Store secret temporarily (not enabled yet)
current_user.totp_secret = secret
db.add(current_user)
db.commit()
# Generate URI for authenticator app
totp = pyotp.TOTP(secret)
uri = totp.provisioning_uri(
name=current_user.email,
issuer_name=settings.APP_NAME
)
# Generate QR code
qr_code = generate_qr_code(uri)
# Log the action
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="2fa_setup_initiated",
resource_type="user",
resource_id=current_user.id,
ip_address=get_client_ip(request),
user_agent=request.headers.get("User-Agent", "")[:500],
status="success"
)
return {
"secret": secret,
"uri": uri,
"qr_code": qr_code
}
@router.post("/verify")
def verify_and_enable_2fa(
request: Request,
verify_request: TOTPVerifyRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> Any:
"""
Verify TOTP code and enable 2FA.
This must be called after setup to complete the 2FA activation.
"""
if current_user.totp_enabled:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="2FA is already enabled"
)
if not current_user.totp_secret:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="2FA setup not initiated. Call /setup first."
)
# Verify the code
totp = pyotp.TOTP(current_user.totp_secret)
if not totp.verify(verify_request.code, valid_window=1):
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="2fa_verify",
resource_type="user",
resource_id=current_user.id,
details={"reason": "invalid_code"},
ip_address=get_client_ip(request),
user_agent=request.headers.get("User-Agent", "")[:500],
status="failure"
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid verification code"
)
# Generate backup codes
backup_codes = generate_backup_codes()
# Enable 2FA
current_user.totp_enabled = True
current_user.backup_codes = backup_codes
db.add(current_user)
db.commit()
# Log the action
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="2fa_enabled",
resource_type="user",
resource_id=current_user.id,
ip_address=get_client_ip(request),
user_agent=request.headers.get("User-Agent", "")[:500],
status="success"
)
return {
"message": "2FA enabled successfully",
"backup_codes": backup_codes
}
@router.post("/disable")
def disable_2fa(
request: Request,
disable_request: TOTPDisableRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> Any:
"""
Disable 2FA. Requires password and current TOTP code.
"""
if not current_user.totp_enabled:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="2FA is not enabled"
)
# Verify password
from app.core.security import verify_password
if not verify_password(disable_request.password, current_user.hashed_password):
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="2fa_disable",
resource_type="user",
resource_id=current_user.id,
details={"reason": "invalid_password"},
ip_address=get_client_ip(request),
user_agent=request.headers.get("User-Agent", "")[:500],
status="failure"
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid password"
)
# Verify TOTP code
totp = pyotp.TOTP(current_user.totp_secret)
if not totp.verify(disable_request.code, valid_window=1):
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="2fa_disable",
resource_type="user",
resource_id=current_user.id,
details={"reason": "invalid_code"},
ip_address=get_client_ip(request),
user_agent=request.headers.get("User-Agent", "")[:500],
status="failure"
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid verification code"
)
# Disable 2FA
current_user.totp_enabled = False
current_user.totp_secret = None
current_user.totp_backup_codes = None
db.add(current_user)
db.commit()
# Log the action
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="2fa_disabled",
resource_type="user",
resource_id=current_user.id,
ip_address=get_client_ip(request),
user_agent=request.headers.get("User-Agent", "")[:500],
status="success"
)
return {"message": "2FA disabled successfully"}
@router.post("/regenerate-backup-codes", response_model=BackupCodesResponse)
def regenerate_backup_codes(
request: Request,
verify_request: TOTPVerifyRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> Any:
"""
Regenerate backup codes. Requires current TOTP code.
"""
if not current_user.totp_enabled:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="2FA is not enabled"
)
# Verify TOTP code
totp = pyotp.TOTP(current_user.totp_secret)
if not totp.verify(verify_request.code, valid_window=1):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid verification code"
)
# Generate new backup codes
backup_codes = generate_backup_codes()
current_user.backup_codes = backup_codes
db.add(current_user)
db.commit()
# Log the action
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="2fa_backup_codes_regenerated",
resource_type="user",
resource_id=current_user.id,
ip_address=get_client_ip(request),
user_agent=request.headers.get("User-Agent", "")[:500],
status="success"
)
return {"backup_codes": backup_codes}
def verify_totp_or_backup(user: User, code: str, db: Session) -> bool:
"""
Verify TOTP code or backup code.
Returns True if valid, False otherwise.
If backup code is used, it's removed from the list.
"""
if not user.totp_enabled or not user.totp_secret:
return True # 2FA not enabled
# Try TOTP verification first
totp = pyotp.TOTP(user.totp_secret)
if totp.verify(code, valid_window=1):
return True
# Try backup code
backup_codes = user.backup_codes
code_upper = code.upper().replace("-", "")
if code_upper in backup_codes:
backup_codes.remove(code_upper)
user.backup_codes = backup_codes
db.add(user)
db.commit()
return True
return False