From 13501561c824b17938767e10ea76ab7130ae8396 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:30:36 -0400 Subject: [PATCH] Add fingerprint management commands and API --- docs/advanced_cli.md | 6 ++++ docs/api_reference.md | 14 +++++++++ src/seedpass/api.py | 34 ++++++++++++++++++++ src/seedpass/cli.py | 21 +++++++++++++ src/tests/test_api_new_endpoints.py | 32 +++++++++++++++++++ src/tests/test_typer_cli.py | 48 +++++++++++++++++++++++++++++ 6 files changed, 155 insertions(+) diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index bba40e8..f540236 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -94,6 +94,9 @@ Manage seed profiles (fingerprints). | Action | Command | Examples | | :--- | :--- | :--- | | List all profiles | `fingerprint list` | `seedpass fingerprint list` | +| Add a profile | `fingerprint add` | `seedpass fingerprint add` | +| Remove a profile | `fingerprint remove` | `seedpass fingerprint remove ` | +| Switch profile | `fingerprint switch` | `seedpass fingerprint switch ` | ### Utility Commands @@ -158,6 +161,9 @@ Code: 123456 ### `fingerprint` Commands - **`seedpass fingerprint list`** – List available profiles by fingerprint. +- **`seedpass fingerprint add`** – Create a new seed profile. +- **`seedpass fingerprint remove `** – Delete the specified profile. +- **`seedpass fingerprint switch `** – Switch the active profile. ### `util` Commands diff --git a/docs/api_reference.md b/docs/api_reference.md index dca3fef..7d21d68 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -24,6 +24,9 @@ Keep this token secret. Every request must include it in the `Authorization` hea - `POST /api/v1/entry/{id}/unarchive` – Unarchive an entry. - `GET /api/v1/config/{key}` – Return the value for a configuration key. - `GET /api/v1/fingerprint` – List available seed fingerprints. +- `POST /api/v1/fingerprint` – Add a new seed fingerprint. +- `DELETE /api/v1/fingerprint/{fp}` – Remove a fingerprint. +- `POST /api/v1/fingerprint/select` – Switch the active fingerprint. - `GET /api/v1/nostr/pubkey` – Fetch the Nostr public key for the active seed. - `POST /api/v1/shutdown` – Stop the server gracefully. @@ -81,6 +84,17 @@ curl -X PUT http://127.0.0.1:8000/api/v1/config/inactivity_timeout \ -d '{"value": 300}' ``` +### Switching Fingerprints + +Change the active seed profile via `POST /api/v1/fingerprint/select`: + +```bash +curl -X POST http://127.0.0.1:8000/api/v1/fingerprint/select \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"fingerprint": "abc123"}' +``` + ### Enabling CORS Cross‑origin requests are disabled by default. Set `SEEDPASS_CORS_ORIGINS` to a comma‑separated list of allowed origins before starting the API: diff --git a/src/seedpass/api.py b/src/seedpass/api.py index 28df1bd..ba6d35a 100644 --- a/src/seedpass/api.py +++ b/src/seedpass/api.py @@ -270,6 +270,40 @@ def list_fingerprints(authorization: str | None = Header(None)) -> List[str]: return _pm.fingerprint_manager.list_fingerprints() +@app.post("/api/v1/fingerprint") +def add_fingerprint(authorization: str | None = Header(None)) -> dict[str, str]: + """Create a new seed profile.""" + _check_token(authorization) + assert _pm is not None + _pm.add_new_fingerprint() + return {"status": "ok"} + + +@app.delete("/api/v1/fingerprint/{fingerprint}") +def remove_fingerprint( + fingerprint: str, authorization: str | None = Header(None) +) -> dict[str, str]: + """Remove a seed profile.""" + _check_token(authorization) + assert _pm is not None + _pm.fingerprint_manager.remove_fingerprint(fingerprint) + return {"status": "deleted"} + + +@app.post("/api/v1/fingerprint/select") +def select_fingerprint( + data: dict, authorization: str | None = Header(None) +) -> dict[str, str]: + """Switch the active seed profile.""" + _check_token(authorization) + assert _pm is not None + fp = data.get("fingerprint") + if not fp: + raise HTTPException(status_code=400, detail="Missing fingerprint") + _pm.select_fingerprint(fp) + return {"status": "ok"} + + @app.get("/api/v1/nostr/pubkey") def get_nostr_pubkey(authorization: str | None = Header(None)) -> Any: _check_token(authorization) diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index 49896a2..cb82902 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -394,6 +394,27 @@ def fingerprint_list(ctx: typer.Context) -> None: typer.echo(fp) +@fingerprint_app.command("add") +def fingerprint_add(ctx: typer.Context) -> None: + """Create a new seed profile.""" + pm = _get_pm(ctx) + pm.add_new_fingerprint() + + +@fingerprint_app.command("remove") +def fingerprint_remove(ctx: typer.Context, fingerprint: str) -> None: + """Remove a seed profile.""" + pm = _get_pm(ctx) + pm.fingerprint_manager.remove_fingerprint(fingerprint) + + +@fingerprint_app.command("switch") +def fingerprint_switch(ctx: typer.Context, fingerprint: str) -> None: + """Switch to another seed profile.""" + pm = _get_pm(ctx) + pm.select_fingerprint(fingerprint) + + @util_app.command("generate-password") def generate_password(ctx: typer.Context, length: int = 24) -> None: """Generate a strong password.""" diff --git a/src/tests/test_api_new_endpoints.py b/src/tests/test_api_new_endpoints.py index a07de53..f4ab8b5 100644 --- a/src/tests/test_api_new_endpoints.py +++ b/src/tests/test_api_new_endpoints.py @@ -109,3 +109,35 @@ def test_update_config_secret_mode(client): assert res.status_code == 200 assert res.json() == {"status": "ok"} assert called["val"] is True + + +def test_fingerprint_endpoints(client): + cl, token = client + calls = {} + + api._pm.add_new_fingerprint = lambda: calls.setdefault("add", True) + api._pm.fingerprint_manager.remove_fingerprint = lambda fp: calls.setdefault( + "remove", fp + ) + api._pm.select_fingerprint = lambda fp: calls.setdefault("select", fp) + + headers = {"Authorization": f"Bearer {token}"} + + res = cl.post("/api/v1/fingerprint", headers=headers) + assert res.status_code == 200 + assert res.json() == {"status": "ok"} + assert calls.get("add") is True + + res = cl.delete("/api/v1/fingerprint/abc", headers=headers) + assert res.status_code == 200 + assert res.json() == {"status": "deleted"} + assert calls.get("remove") == "abc" + + res = cl.post( + "/api/v1/fingerprint/select", + json={"fingerprint": "xyz"}, + headers=headers, + ) + assert res.status_code == 200 + assert res.json() == {"status": "ok"} + assert calls.get("select") == "xyz" diff --git a/src/tests/test_typer_cli.py b/src/tests/test_typer_cli.py index 148c222..603b728 100644 --- a/src/tests/test_typer_cli.py +++ b/src/tests/test_typer_cli.py @@ -105,6 +105,54 @@ def test_fingerprint_list(monkeypatch): assert "a" in result.stdout and "b" in result.stdout +def test_fingerprint_add(monkeypatch): + called = {} + + def add(): + called["add"] = True + + pm = SimpleNamespace( + add_new_fingerprint=add, + select_fingerprint=lambda fp: None, + fingerprint_manager=SimpleNamespace(), + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["fingerprint", "add"]) + assert result.exit_code == 0 + assert called.get("add") is True + + +def test_fingerprint_remove(monkeypatch): + called = {} + + def remove(fp): + called["fp"] = fp + + pm = SimpleNamespace( + fingerprint_manager=SimpleNamespace(remove_fingerprint=remove), + select_fingerprint=lambda fp: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["fingerprint", "remove", "abc"]) + assert result.exit_code == 0 + assert called.get("fp") == "abc" + + +def test_fingerprint_switch(monkeypatch): + called = {} + + def switch(fp): + called["fp"] = fp + + pm = SimpleNamespace( + select_fingerprint=switch, fingerprint_manager=SimpleNamespace() + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["fingerprint", "switch", "def"]) + assert result.exit_code == 0 + assert called.get("fp") == "def" + + def test_config_get(monkeypatch): pm = SimpleNamespace( config_manager=SimpleNamespace(