mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Merge pull request #437 from PR0M3TH3AN/codex/add-vault-import-command-and-api
Add vault import command and API
This commit is contained in:
@@ -68,6 +68,7 @@ Manage the entire vault for a profile.
|
||||
| Action | Command | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| Export the vault | `vault export` | `seedpass vault export --file backup.json` |
|
||||
| Import a vault | `vault import` | `seedpass vault import --file backup.json` |
|
||||
| Change the master password | `vault change-password` | `seedpass vault change-password` |
|
||||
|
||||
### Nostr Commands
|
||||
@@ -150,6 +151,7 @@ Code: 123456
|
||||
### `vault` Commands
|
||||
|
||||
- **`seedpass vault export`** – Export the entire vault to an encrypted JSON file.
|
||||
- **`seedpass vault import`** – Import a vault from an encrypted JSON file.
|
||||
- **`seedpass vault change-password`** – Change the master password used for encryption.
|
||||
|
||||
### `nostr` Commands
|
||||
|
@@ -31,6 +31,7 @@ Keep this token secret. Every request must include it in the `Authorization` hea
|
||||
- `POST /api/v1/checksum/verify` – Verify the checksum of the running script.
|
||||
- `POST /api/v1/checksum/update` – Update the stored script checksum.
|
||||
- `POST /api/v1/change-password` – Change the master password for the active profile.
|
||||
- `POST /api/v1/vault/import` – Import a vault backup from a file or path.
|
||||
- `POST /api/v1/shutdown` – Stop the server gracefully.
|
||||
|
||||
## Example Requests
|
||||
|
@@ -29,3 +29,4 @@ fastapi>=0.116.0
|
||||
uvicorn>=0.35.0
|
||||
httpx>=0.28.1
|
||||
requests>=2.32
|
||||
python-multipart
|
||||
|
@@ -3,10 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import FastAPI, Header, HTTPException
|
||||
from fastapi import FastAPI, Header, HTTPException, Request
|
||||
import asyncio
|
||||
import sys
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
@@ -329,6 +331,37 @@ def update_checksum(authorization: str | None = Header(None)) -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/v1/vault/import")
|
||||
async def import_vault(
|
||||
request: Request, authorization: str | None = Header(None)
|
||||
) -> dict[str, str]:
|
||||
"""Import a vault backup from a file upload or a server path."""
|
||||
_check_token(authorization)
|
||||
assert _pm is not None
|
||||
|
||||
ctype = request.headers.get("content-type", "")
|
||||
if ctype.startswith("multipart/form-data"):
|
||||
form = await request.form()
|
||||
file = form.get("file")
|
||||
if file is None:
|
||||
raise HTTPException(status_code=400, detail="Missing file")
|
||||
data = await file.read()
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||
tmp.write(data)
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
_pm.handle_import_database(tmp_path)
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
else:
|
||||
body = await request.json()
|
||||
path = body.get("path")
|
||||
if not path:
|
||||
raise HTTPException(status_code=400, detail="Missing file or path")
|
||||
_pm.handle_import_database(Path(path))
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/v1/change-password")
|
||||
def change_password(authorization: str | None = Header(None)) -> dict[str, str]:
|
||||
"""Change the master password for the active profile."""
|
||||
|
@@ -324,6 +324,16 @@ def vault_export(
|
||||
typer.echo(str(file))
|
||||
|
||||
|
||||
@vault_app.command("import")
|
||||
def vault_import(
|
||||
ctx: typer.Context, file: str = typer.Option(..., help="Input file")
|
||||
) -> None:
|
||||
"""Import a vault from an encrypted JSON file."""
|
||||
pm = _get_pm(ctx)
|
||||
pm.handle_import_database(Path(file))
|
||||
typer.echo(str(file))
|
||||
|
||||
|
||||
@vault_app.command("change-password")
|
||||
def vault_change_password(ctx: typer.Context) -> None:
|
||||
"""Change the master password used for encryption."""
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from seedpass import api
|
||||
@@ -161,3 +162,48 @@ def test_checksum_endpoints(client):
|
||||
assert res.status_code == 200
|
||||
assert res.json() == {"status": "ok"}
|
||||
assert calls.get("update") is True
|
||||
|
||||
|
||||
def test_vault_import_via_path(client, tmp_path):
|
||||
cl, token = client
|
||||
called = {}
|
||||
|
||||
def import_db(path):
|
||||
called["path"] = path
|
||||
|
||||
api._pm.handle_import_database = import_db
|
||||
file_path = tmp_path / "b.json"
|
||||
file_path.write_text("{}")
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
res = cl.post(
|
||||
"/api/v1/vault/import",
|
||||
json={"path": str(file_path)},
|
||||
headers=headers,
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert res.json() == {"status": "ok"}
|
||||
assert called["path"] == file_path
|
||||
|
||||
|
||||
def test_vault_import_via_upload(client, tmp_path):
|
||||
cl, token = client
|
||||
called = {}
|
||||
|
||||
def import_db(path):
|
||||
called["path"] = path
|
||||
|
||||
api._pm.handle_import_database = import_db
|
||||
file_path = tmp_path / "c.json"
|
||||
file_path.write_text("{}")
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
with open(file_path, "rb") as fh:
|
||||
res = cl.post(
|
||||
"/api/v1/vault/import",
|
||||
files={"file": ("c.json", fh.read())},
|
||||
headers=headers,
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert res.json() == {"status": "ok"}
|
||||
assert isinstance(called.get("path"), Path)
|
||||
|
@@ -81,6 +81,23 @@ def test_vault_export(monkeypatch, tmp_path):
|
||||
assert called["path"] == out_path
|
||||
|
||||
|
||||
def test_vault_import(monkeypatch, tmp_path):
|
||||
called = {}
|
||||
|
||||
def import_db(path):
|
||||
called["path"] = path
|
||||
|
||||
pm = SimpleNamespace(
|
||||
handle_import_database=import_db, select_fingerprint=lambda fp: None
|
||||
)
|
||||
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||
in_path = tmp_path / "in.json"
|
||||
in_path.write_text("{}")
|
||||
result = runner.invoke(app, ["vault", "import", "--file", str(in_path)])
|
||||
assert result.exit_code == 0
|
||||
assert called["path"] == in_path
|
||||
|
||||
|
||||
def test_vault_change_password(monkeypatch):
|
||||
called = {}
|
||||
|
||||
|
Reference in New Issue
Block a user