feat(cli): octop captcha reset — offline lockout escape hatch

Clears the stored captcha settings blob so login falls back to the
built-in slider when a misconfigured provider blocks all logins
(settings UI requires login, so it cannot fix itself). Boot-env
captcha is untouched; a running server needs a restart.
CHANGELOG: cover reset command, TC3 switch, widget language, v2 unlist.
This commit is contained in:
HUANG Cheng
2026-09-14 11:46:21 +08:00
parent 56f56ddcb4
commit c5a7649d24
2 changed files with 85 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
"""`octop captcha` — offline captcha maintenance (lockout escape hatch)."""
from __future__ import annotations
import click
from octop.cli.support.db import open_cli_services
from octop.infra.auth.captcha.store import SETTINGS_KEY
@click.group("captcha")
def captcha() -> None:
"""Login captcha maintenance against the local database."""
@captcha.command("reset")
def reset() -> None:
"""Clear stored captcha settings so login falls back to the slider default.
Use this when a misconfigured provider (wrong keys, unreachable vendor,
hostname not allowlisted) locks everyone out of the dashboard: the
settings UI requires login, so the only offline escape is this command
or restoring a backup. Boot-env captcha (OCTOP_CAPTCHA_*) is not touched
— unset those in the environment file if they are the lockout cause.
If `octop run` is already running, restart it for the change to apply.
"""
with open_cli_services() as services:
if services.settings_repo.get(SETTINGS_KEY) is None:
click.echo("captcha settings already at default (nothing stored)")
return
services.settings_repo.delete(SETTINGS_KEY)
click.echo(
"captcha settings cleared; login now uses the built-in slider "
"(restart `octop run` if it is running)"
)
+50
View File
@@ -0,0 +1,50 @@
"""Tests for `octop captcha reset` (lockout escape hatch)."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from click.testing import CliRunner
from octop.cli.main import cli
from octop.cli.support.db import open_cli_services
from octop.infra.auth.captcha.config import snapshot_env
from octop.infra.auth.captcha.store import SETTINGS_KEY, load_effective
@pytest.fixture
def octop_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
monkeypatch.setenv("OCTOP_HOME", str(tmp_path))
return tmp_path
def _seed_blob(active: str = "recaptcha-v3") -> None:
with open_cli_services() as services:
services.settings_repo.set(
SETTINGS_KEY,
json.dumps(
{
"active": active,
"providers": {active: {"site_key": "k", "secret_enc": ""}},
}
),
)
def test_reset_clears_stored_settings_and_falls_back_to_slider(octop_home: Path) -> None:
_seed_blob()
r = CliRunner().invoke(cli, ["captcha", "reset"])
assert r.exit_code == 0, r.output
with open_cli_services() as services:
assert services.settings_repo.get(SETTINGS_KEY) is None
effective = load_effective(services.settings_repo, services.secret_repo, snapshot_env({}))
assert effective.slug == "slider"
assert "slider" in r.output
def test_reset_is_idempotent(octop_home: Path) -> None:
r = CliRunner().invoke(cli, ["captcha", "reset"])
assert r.exit_code == 0, r.output
assert "already at default" in r.output