Add secret mode toggle command and API

This commit is contained in:
thePR0M3TH3AN
2025-07-09 20:40:44 -04:00
parent 8aac7906d9
commit 19e7ac18ca
6 changed files with 152 additions and 0 deletions

View File

@@ -171,6 +171,7 @@ Code: 123456
- **`seedpass config get <key>`** Retrieve a configuration value such as `inactivity_timeout`, `secret_mode`, or `auto_sync`.
- **`seedpass config set <key> <value>`** 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

View File

@@ -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 <token>" \
-H "Content-Type: application/json" \
-d '{"enabled": true, "delay": 20}'
```
### Switching Fingerprints
Change the active seed profile via `POST /api/v1/fingerprint/select`:

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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

View File

@@ -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