From 19e7ac18ca719f5927c9e22f3e5bcca72e4e6164 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:40:44 -0400 Subject: [PATCH] Add secret mode toggle command and API --- docs/advanced_cli.md | 1 + docs/api_reference.md | 12 ++++++ src/seedpass/api.py | 19 +++++++++ src/seedpass/cli.py | 51 ++++++++++++++++++++++++ src/tests/test_api_new_endpoints.py | 25 ++++++++++++ src/tests/test_cli_toggle_secret_mode.py | 44 ++++++++++++++++++++ 6 files changed, 152 insertions(+) create mode 100644 src/tests/test_cli_toggle_secret_mode.py diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index 63f84b7..0148aa5 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -171,6 +171,7 @@ Code: 123456 - **`seedpass config get `** – Retrieve a configuration value such as `inactivity_timeout`, `secret_mode`, or `auto_sync`. - **`seedpass config set `** – Update a configuration option. Example: `seedpass config set inactivity_timeout 300`. +- **`seedpass config toggle-secret-mode`** – Interactively enable or disable Secret Mode and set the clipboard delay. ### `fingerprint` Commands diff --git a/docs/api_reference.md b/docs/api_reference.md index 03e6532..ec6619d 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -20,6 +20,7 @@ Keep this token secret. Every request must include it in the `Authorization` hea - `POST /api/v1/entry` – Create a new entry of any supported type. - `PUT /api/v1/entry/{id}` – Modify an existing entry. - `PUT /api/v1/config/{key}` – Update a configuration value. +- `POST /api/v1/secret-mode` – Enable or disable Secret Mode and set the clipboard delay. - `POST /api/v1/entry/{id}/archive` – Archive an entry. - `POST /api/v1/entry/{id}/unarchive` – Unarchive an entry. - `GET /api/v1/config/{key}` – Return the value for a configuration key. @@ -95,6 +96,17 @@ curl -X PUT http://127.0.0.1:8000/api/v1/config/inactivity_timeout \ -d '{"value": 300}' ``` +### Toggling Secret Mode + +Send both `enabled` and `delay` values to `/api/v1/secret-mode`: + +```bash +curl -X POST http://127.0.0.1:8000/api/v1/secret-mode \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"enabled": true, "delay": 20}' +``` + ### Switching Fingerprints Change the active seed profile via `POST /api/v1/fingerprint/select`: diff --git a/src/seedpass/api.py b/src/seedpass/api.py index 776a62f..6827934 100644 --- a/src/seedpass/api.py +++ b/src/seedpass/api.py @@ -266,6 +266,25 @@ def update_config( return {"status": "ok"} +@app.post("/api/v1/secret-mode") +def set_secret_mode( + data: dict, authorization: str | None = Header(None) +) -> dict[str, str]: + """Enable/disable secret mode and set the clipboard delay.""" + _check_token(authorization) + assert _pm is not None + enabled = data.get("enabled") + delay = data.get("delay") + if enabled is None or delay is None: + raise HTTPException(status_code=400, detail="Missing fields") + cfg = _pm.config_manager + cfg.set_secret_mode_enabled(bool(enabled)) + cfg.set_clipboard_clear_delay(int(delay)) + _pm.secret_mode_enabled = bool(enabled) + _pm.clipboard_clear_delay = int(delay) + return {"status": "ok"} + + @app.get("/api/v1/fingerprint") def list_fingerprints(authorization: str | None = Header(None)) -> List[str]: _check_token(authorization) diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index 6a99321..e04f6bd 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -450,6 +450,57 @@ def config_set(ctx: typer.Context, key: str, value: str) -> None: typer.echo("Updated") +@config_app.command("toggle-secret-mode") +def config_toggle_secret_mode(ctx: typer.Context) -> None: + """Interactively enable or disable secret mode.""" + pm = _get_pm(ctx) + cfg = pm.config_manager + try: + enabled = cfg.get_secret_mode_enabled() + delay = cfg.get_clipboard_clear_delay() + except Exception as exc: # pragma: no cover - pass through errors + typer.echo(f"Error loading settings: {exc}") + raise typer.Exit(code=1) + + typer.echo(f"Secret mode is currently {'ON' if enabled else 'OFF'}") + choice = ( + typer.prompt( + "Enable secret mode? (y/n, blank to keep)", default="", show_default=False + ) + .strip() + .lower() + ) + if choice in ("y", "yes"): + enabled = True + elif choice in ("n", "no"): + enabled = False + + inp = typer.prompt( + f"Clipboard clear delay in seconds [{delay}]", default="", show_default=False + ).strip() + if inp: + try: + delay = int(inp) + if delay <= 0: + typer.echo("Delay must be positive") + raise typer.Exit(code=1) + except ValueError: + typer.echo("Invalid number") + raise typer.Exit(code=1) + + try: + cfg.set_secret_mode_enabled(enabled) + cfg.set_clipboard_clear_delay(delay) + pm.secret_mode_enabled = enabled + pm.clipboard_clear_delay = delay + except Exception as exc: # pragma: no cover - pass through errors + typer.echo(f"Error: {exc}") + raise typer.Exit(code=1) + + status = "enabled" if enabled else "disabled" + typer.echo(f"Secret mode {status}.") + + @fingerprint_app.command("list") def fingerprint_list(ctx: typer.Context) -> None: """List available seed profiles.""" diff --git a/src/tests/test_api_new_endpoints.py b/src/tests/test_api_new_endpoints.py index fbe09b4..fc187db 100644 --- a/src/tests/test_api_new_endpoints.py +++ b/src/tests/test_api_new_endpoints.py @@ -275,3 +275,28 @@ def test_vault_lock_endpoint(client): api._pm.unlock_vault = lambda: setattr(api._pm, "locked", False) api._pm.unlock_vault() assert api._pm.locked is False + + +def test_secret_mode_endpoint(client): + cl, token = client + called = {} + + def set_secret(val): + called.setdefault("enabled", val) + + def set_delay(val): + called.setdefault("delay", val) + + api._pm.config_manager.set_secret_mode_enabled = set_secret + api._pm.config_manager.set_clipboard_clear_delay = set_delay + + headers = {"Authorization": f"Bearer {token}"} + res = cl.post( + "/api/v1/secret-mode", + json={"enabled": True, "delay": 12}, + headers=headers, + ) + assert res.status_code == 200 + assert res.json() == {"status": "ok"} + assert called["enabled"] is True + assert called["delay"] == 12 diff --git a/src/tests/test_cli_toggle_secret_mode.py b/src/tests/test_cli_toggle_secret_mode.py new file mode 100644 index 0000000..883afb0 --- /dev/null +++ b/src/tests/test_cli_toggle_secret_mode.py @@ -0,0 +1,44 @@ +import types +from types import SimpleNamespace +from typer.testing import CliRunner + +from seedpass.cli import app +from seedpass import cli + +runner = CliRunner() + + +def _make_pm(called, enabled=False, delay=45): + cfg = SimpleNamespace( + get_secret_mode_enabled=lambda: enabled, + get_clipboard_clear_delay=lambda: delay, + set_secret_mode_enabled=lambda v: called.setdefault("enabled", v), + set_clipboard_clear_delay=lambda v: called.setdefault("delay", v), + ) + pm = SimpleNamespace( + config_manager=cfg, + secret_mode_enabled=enabled, + clipboard_clear_delay=delay, + select_fingerprint=lambda fp: None, + ) + return pm + + +def test_toggle_secret_mode_updates(monkeypatch): + called = {} + pm = _make_pm(called) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["config", "toggle-secret-mode"], input="y\n10\n") + assert result.exit_code == 0 + assert called == {"enabled": True, "delay": 10} + assert "Secret mode enabled." in result.stdout + + +def test_toggle_secret_mode_keep(monkeypatch): + called = {} + pm = _make_pm(called, enabled=True, delay=30) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["config", "toggle-secret-mode"], input="\n\n") + assert result.exit_code == 0 + assert called == {"enabled": True, "delay": 30} + assert "Secret mode enabled." in result.stdout