Add vault import support

This commit is contained in:
thePR0M3TH3AN
2025-07-09 16:45:32 -04:00
parent 8ebbeab65c
commit 068611a02c
7 changed files with 111 additions and 1 deletions

View File

@@ -29,3 +29,4 @@ fastapi>=0.116.0
uvicorn>=0.35.0
httpx>=0.28.1
requests>=2.32
python-multipart

View File

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

View File

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

View File

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

View File

@@ -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 = {}