diff --git a/containers/backupchecks/src/backend/app/auth/routes.py b/containers/backupchecks/src/backend/app/auth/routes.py index 39f7e6e..a6517f0 100644 --- a/containers/backupchecks/src/backend/app/auth/routes.py +++ b/containers/backupchecks/src/backend/app/auth/routes.py @@ -163,10 +163,21 @@ def _groups_from_claims(claims: dict) -> set[str]: return set() +def _captcha_enabled() -> bool: + """Return True when the login captcha is enabled in SystemSettings.""" + try: + s = SystemSettings.query.first() + if s is None: + return True # default on for new installs + return bool(getattr(s, "login_captcha_enabled", True)) + except Exception: + return True # fail-safe: keep captcha on if DB unreachable + + def captcha_required(func): @wraps(func) def wrapper(*args, **kwargs): - if request.method == "POST": + if request.method == "POST" and _captcha_enabled(): expected = session.get("captcha_answer") provided = (request.form.get("captcha") or "").strip() if not expected or provided != expected: @@ -184,6 +195,7 @@ def captcha_required(func): return render_template( "auth/login.html", captcha_question=question, + captcha_enabled=True, username=request.form.get("username", ""), entra_sso_enabled=entra_ready, ) @@ -195,6 +207,8 @@ def captcha_required(func): @auth_bp.route("/login", methods=["GET", "POST"]) @captcha_required def login(): + captcha_on = _captcha_enabled() + if request.method == "GET": if not users_exist(): return redirect(url_for("auth.initial_setup")) @@ -211,6 +225,7 @@ def login(): return render_template( "auth/login.html", captcha_question=question, + captcha_enabled=captcha_on, entra_sso_enabled=entra_ready, ) @@ -233,6 +248,7 @@ def login(): return render_template( "auth/login.html", captcha_question=question, + captcha_enabled=captcha_on, username=username, entra_sso_enabled=entra_ready, ) diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index 4ac66bf..b517e89 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -741,6 +741,7 @@ def settings(): old_ui_timezone = settings.ui_timezone old_require_daily_dashboard_visit = settings.require_daily_dashboard_visit old_is_sandbox_environment = settings.is_sandbox_environment + old_login_captcha_enabled = getattr(settings, "login_captcha_enabled", True) old_graph_tenant_id = settings.graph_tenant_id old_graph_client_id = settings.graph_client_id old_graph_mailbox = settings.graph_mailbox @@ -787,6 +788,9 @@ def settings(): # Checkbox: present in form = checked, absent = unchecked. settings.is_sandbox_environment = bool(request.form.get("is_sandbox_environment")) + # Login captcha toggle — same form (General tab). + settings.login_captcha_enabled = bool(request.form.get("login_captcha_enabled")) + # Autotask integration if "autotask_enabled" in request.form: settings.autotask_enabled = bool(request.form.get("autotask_enabled")) @@ -981,6 +985,8 @@ def settings(): changes_general["require_daily_dashboard_visit"] = {"old": old_require_daily_dashboard_visit, "new": settings.require_daily_dashboard_visit} if old_is_sandbox_environment != settings.is_sandbox_environment: changes_general["is_sandbox_environment"] = {"old": old_is_sandbox_environment, "new": settings.is_sandbox_environment} + if old_login_captcha_enabled != settings.login_captcha_enabled: + changes_general["login_captcha_enabled"] = {"old": old_login_captcha_enabled, "new": settings.login_captcha_enabled} if changes_general: _log_admin_event( diff --git a/containers/backupchecks/src/backend/app/migrations.py b/containers/backupchecks/src/backend/app/migrations.py index fc8ff8c..8cc8427 100644 --- a/containers/backupchecks/src/backend/app/migrations.py +++ b/containers/backupchecks/src/backend/app/migrations.py @@ -1447,6 +1447,7 @@ def run_migrations() -> None: migrate_cc_accounts_repo_unique_key() migrate_cc_remove_synthetic_missed_runs() migrate_entra_sso_settings() + migrate_system_settings_login_captcha() print("[migrations] All migrations completed.") @@ -1554,6 +1555,37 @@ def migrate_system_settings_sandbox_environment() -> None: print(f"[migrations] Failed to migrate system_settings.is_sandbox_environment: {exc}") +def migrate_system_settings_login_captcha() -> None: + """Add login_captcha_enabled column to system_settings if missing. + + Default TRUE so existing installs keep captcha enabled after the upgrade. + """ + table = "system_settings" + column = "login_captcha_enabled" + + try: + engine = db.get_engine() + except Exception as exc: + print(f"[migrations] Could not get engine for login_captcha_enabled migration: {exc}") + return + + try: + if _column_exists(table, column): + print("[migrations] system_settings.login_captcha_enabled already exists.") + return + + with engine.begin() as conn: + conn.execute( + text( + f'ALTER TABLE "{table}" ADD COLUMN {column} BOOLEAN NOT NULL DEFAULT TRUE' + ) + ) + + print("[migrations] migrate_system_settings_login_captcha completed.") + except Exception as exc: + print(f"[migrations] Failed to migrate system_settings.login_captcha_enabled: {exc}") + + def migrate_performance_indexes() -> None: """Add performance indexes for frequently queried foreign key columns. diff --git a/containers/backupchecks/src/backend/app/models.py b/containers/backupchecks/src/backend/app/models.py index 83d2f64..e80ebe9 100644 --- a/containers/backupchecks/src/backend/app/models.py +++ b/containers/backupchecks/src/backend/app/models.py @@ -123,6 +123,9 @@ class SystemSettings(db.Model): # this is not a production environment. is_sandbox_environment = db.Column(db.Boolean, nullable=False, default=False) + # Login page captcha (simple math question). Default True for new installs. + login_captcha_enabled = db.Column(db.Boolean, nullable=False, default=True) + # Cove Data Protection integration settings cove_enabled = db.Column(db.Boolean, nullable=False, default=False) cove_api_url = db.Column(db.String(255), nullable=True) # default: https://api.backup.management/jsonapi diff --git a/containers/backupchecks/src/templates/auth/login.html b/containers/backupchecks/src/templates/auth/login.html index 6eadd04..daf32f0 100644 --- a/containers/backupchecks/src/templates/auth/login.html +++ b/containers/backupchecks/src/templates/auth/login.html @@ -25,6 +25,7 @@ required /> + {% if captcha_enabled %}