mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Add fingerprint management commands and API
This commit is contained in:
@@ -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 <fp>` |
|
||||
| Switch profile | `fingerprint switch` | `seedpass fingerprint switch <fp>` |
|
||||
|
||||
### 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 <fp>`** – Delete the specified profile.
|
||||
- **`seedpass fingerprint switch <fp>`** – Switch the active profile.
|
||||
|
||||
### `util` Commands
|
||||
|
||||
|
@@ -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 <token>" \
|
||||
-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:
|
||||
|
@@ -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)
|
||||
|
@@ -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."""
|
||||
|
@@ -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"
|
||||
|
@@ -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(
|
||||
|
Reference in New Issue
Block a user