diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index f8e3b75..63f84b7 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -73,6 +73,7 @@ Manage the entire vault for a profile. | Import a vault | `vault import` | `seedpass vault import --file backup.json` | | Change the master password | `vault change-password` | `seedpass vault change-password` | | Lock the vault | `vault lock` | `seedpass vault lock` | +| Show profile statistics | `vault stats` | `seedpass vault stats` | ### Nostr Commands @@ -159,6 +160,7 @@ Code: 123456 - **`seedpass vault import`** – Import a vault from an encrypted JSON file. - **`seedpass vault change-password`** – Change the master password used for encryption. - **`seedpass vault lock`** – Clear sensitive data from memory and require reauthentication. +- **`seedpass vault stats`** – Display statistics about the active seed profile. ### `nostr` Commands diff --git a/docs/api_reference.md b/docs/api_reference.md index 1179fea..03e6532 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -29,6 +29,7 @@ Keep this token secret. Every request must include it in the `Authorization` hea - `POST /api/v1/fingerprint/select` – Switch the active fingerprint. - `GET /api/v1/totp/export` – Export all TOTP entries as JSON. - `GET /api/v1/totp` – Return current TOTP codes and remaining time. +- `GET /api/v1/stats` – Return statistics about the active seed profile. - `GET /api/v1/parent-seed` – Reveal the parent seed or save it with `?file=`. - `GET /api/v1/nostr/pubkey` – Fetch the Nostr public key for the active seed. - `POST /api/v1/checksum/verify` – Verify the checksum of the running script. diff --git a/src/seedpass/api.py b/src/seedpass/api.py index 65333b6..776a62f 100644 --- a/src/seedpass/api.py +++ b/src/seedpass/api.py @@ -333,6 +333,14 @@ def get_totp_codes(authorization: str | None = Header(None)) -> dict: return {"codes": codes} +@app.get("/api/v1/stats") +def get_profile_stats(authorization: str | None = Header(None)) -> dict: + """Return statistics about the active seed profile.""" + _check_token(authorization) + assert _pm is not None + return _pm.get_profile_stats() + + @app.get("/api/v1/parent-seed") def get_parent_seed( authorization: str | None = Header(None), file: str | None = None diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index 4941dc5..6a99321 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -368,6 +368,14 @@ def vault_lock(ctx: typer.Context) -> None: typer.echo("locked") +@vault_app.command("stats") +def vault_stats(ctx: typer.Context) -> None: + """Display statistics about the current seed profile.""" + pm = _get_pm(ctx) + stats = pm.get_profile_stats() + typer.echo(json.dumps(stats, indent=2)) + + @vault_app.command("reveal-parent-seed") def vault_reveal_parent_seed( ctx: typer.Context, diff --git a/src/tests/test_api_profile_stats.py b/src/tests/test_api_profile_stats.py new file mode 100644 index 0000000..153dbd3 --- /dev/null +++ b/src/tests/test_api_profile_stats.py @@ -0,0 +1,13 @@ +from test_api import client + + +def test_profile_stats_endpoint(client): + cl, token = client + stats = {"total_entries": 1} + # monkeypatch set _pm.get_profile_stats after client fixture started + import seedpass.api as api + + api._pm.get_profile_stats = lambda: stats + res = cl.get("/api/v1/stats", headers={"Authorization": f"Bearer {token}"}) + assert res.status_code == 200 + assert res.json() == stats diff --git a/src/tests/test_cli_vault_stats.py b/src/tests/test_cli_vault_stats.py new file mode 100644 index 0000000..0e47956 --- /dev/null +++ b/src/tests/test_cli_vault_stats.py @@ -0,0 +1,25 @@ +import json +from types import SimpleNamespace +from typer.testing import CliRunner + +from seedpass.cli import app +from seedpass import cli + +runner = CliRunner() + + +def test_vault_stats_command(monkeypatch): + stats = { + "total_entries": 2, + "entries": {"password": 1, "totp": 1}, + } + pm = SimpleNamespace( + get_profile_stats=lambda: stats, select_fingerprint=lambda fp: None + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["vault", "stats"]) + assert result.exit_code == 0 + out = result.stdout + # Output should be pretty JSON with the expected values + data = json.loads(out) + assert data == stats