diff --git a/app/api/qr_auth.py b/app/api/qr_auth.py index 8f891e4e..97aadb94 100644 --- a/app/api/qr_auth.py +++ b/app/api/qr_auth.py @@ -31,6 +31,7 @@ from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.auth import require_login +from app.config import settings from app.database import get_db from app.middleware.audit_log import get_client_ip from app.utils.session_manager import ( @@ -151,6 +152,11 @@ async def create_challenge( displayed to the user. The mobile app scans this QR code and calls the ``/claim`` endpoint. """ + if not settings.qr_login_enabled: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="QR login feature is currently disabled. Please contact your administrator to enable it.", + ) ip = get_client_ip(request) challenge = create_qr_challenge(db, owner_id, ip_address=ip) @@ -187,6 +193,11 @@ async def poll_challenge_status( The web UI calls this endpoint every few seconds to check if the mobile app has scanned the QR code and claimed the challenge. """ + if not settings.qr_login_enabled: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="QR login feature is currently disabled. Please contact your administrator to enable it.", + ) result = get_challenge_status(db, challenge_id, owner_id) if not result: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Challenge not found") @@ -206,6 +217,11 @@ async def claim_challenge( serves as proof that the user authorized this login from their web session. """ + if not settings.qr_login_enabled: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="QR login feature is currently disabled. Please contact your administrator to enable it.", + ) ip = get_client_ip(request) result = claim_qr_challenge(db, body.challenge_token, device_name=body.device_name, ip_address=ip) diff --git a/app/config.py b/app/config.py index aabfdff0..85631fb8 100644 --- a/app/config.py +++ b/app/config.py @@ -244,6 +244,10 @@ class Settings(BaseSettings): "Useful for admin-configured non-standard durations." ), ) + qr_login_enabled: bool = Field( + default=True, + description="Enable QR code-based login for mobile device authentication (default: True).", + ) qr_login_challenge_ttl_seconds: int = Field( default=120, description="Time-to-live in seconds for QR login challenges (default: 2 minutes).", diff --git a/app/utils/settings_service.py b/app/utils/settings_service.py index e4961dab..e5d37985 100644 --- a/app/utils/settings_service.py +++ b/app/utils/settings_service.py @@ -206,6 +206,14 @@ SETTING_METADATA = { "required": False, "restart_required": True, }, + "qr_login_enabled": { + "category": "Authentication", + "description": "Enable QR code-based login for mobile device authentication.", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, "qr_login_challenge_ttl_seconds": { "category": "Authentication", "description": "Time-to-live in seconds for QR login challenges (default 120).", diff --git a/app/views/settings.py b/app/views/settings.py index a026a124..85307a82 100644 --- a/app/views/settings.py +++ b/app/views/settings.py @@ -464,7 +464,7 @@ async def connections_page(request: Request, db: Session = Depends(get_db)): # Feature toggles sso_auto_login = _is_truthy(_get_effective("sso_auto_login")) - qr_login_enabled = _is_truthy(_get_effective("qr_login_challenge_ttl_seconds")) + qr_login_enabled = _is_truthy(_get_effective("qr_login_enabled")) frontend_url_configured = bool(_get_effective("public_base_url")) return templates.TemplateResponse( diff --git a/frontend/templates/admin_connections.html b/frontend/templates/admin_connections.html index 3e9db7c2..232663ac 100644 --- a/frontend/templates/admin_connections.html +++ b/frontend/templates/admin_connections.html @@ -46,7 +46,8 @@