feat(seed): admin import/export API for seed data (PSA-52 part 1) #344

Merged
nrupard merged 3 commits from feat/PSA-52-admin-import-export into main 2026-07-06 17:41:42 +02:00
Owner

The API layer of the in-app import/export capability (David's convergence: seeding is the same mechanism as importing a customer's data). Makes the canonical seed format round-trippable and exposes admin-gated import + export endpoints. The web admin UI and Yousif's first-run setup choices land in part 2.

What's here

  • Schema now derives Serialize + PartialEq, so a SeedFile can be written back out. canonical_format_round_trips proves parse -> serialize -> parse reproduces it - the guarantee behind "export then import reproduces state".
  • seed::export serializes current seed-owned data back into a SeedFile: users + feedback under the reserved domain (new UserRepository::list_seed_users / FeedbackRepository::list_seed_by_domain, the same suffix scope reset uses), plus the full catalog of groups/applications. Password hashes are never emitted (no schema field for them), so a re-import assigns default_password; exported accounts should be treated as needing a reset.
  • GET /v1/admin/seed/export - JSON file download (read-only, admin-gated).
  • POST /v1/admin/seed/import - loads a posted file through the shared loader (admin-gated). Additionally runs seed_guard, so demo data can never be written into a production/unset environment even by an admin.

Scope

Scoped to seed-owned data (reserved domain) so a round trip re-imports cleanly. General customer-data import/export (arbitrary domains, password sourcing) depends on the PSA-56 owns / password_env generalizations.

Part 2 (follow-up): the web admin import/export page (upload + download) and first-run setup choices (start empty / load template / custom), with demo-msp + minimal templates.

Acceptance criteria (this PR)

  • Admin can export current data to a canonical file with secrets redacted (no hashes/tokens/Stripe secrets - structural, the schema has no such fields): GET /v1/admin/seed/export.
  • Admin can import a canonical file via the shared loader: POST /v1/admin/seed/import.
  • Round trip (export -> import) reproduces the exported state: proven by canonical_format_round_trips at the format level; DB round-trip runs against a dev database.
  • Import/export are admin-only; import is additionally prod-guarded.

Still open (part 2): the admin UI and first-run setup choices.

Verification

just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; seed lib 54 tests incl. the round-trip). Endpoints + export mapper are compile- and clippy-checked; DB behavior runs against a dev Postgres (the container check has none).

#PSA-52

The API layer of the in-app import/export capability (David's convergence: seeding is the same mechanism as importing a customer's data). Makes the canonical seed format round-trippable and exposes admin-gated import + export endpoints. The web admin UI and Yousif's first-run setup choices land in part 2. ## What's here - Schema now derives `Serialize` + `PartialEq`, so a `SeedFile` can be written back out. `canonical_format_round_trips` proves parse -> serialize -> parse reproduces it - the guarantee behind "export then import reproduces state". - `seed::export` serializes current seed-owned data back into a `SeedFile`: users + feedback under the reserved domain (new `UserRepository::list_seed_users` / `FeedbackRepository::list_seed_by_domain`, the same suffix scope reset uses), plus the full catalog of groups/applications. Password hashes are never emitted (no schema field for them), so a re-import assigns `default_password`; exported accounts should be treated as needing a reset. - `GET /v1/admin/seed/export` - JSON file download (read-only, admin-gated). - `POST /v1/admin/seed/import` - loads a posted file through the shared loader (admin-gated). Additionally runs `seed_guard`, so demo data can never be written into a production/unset environment even by an admin. ## Scope Scoped to seed-owned data (reserved domain) so a round trip re-imports cleanly. General customer-data import/export (arbitrary domains, password sourcing) depends on the PSA-56 `owns` / `password_env` generalizations. Part 2 (follow-up): the web admin import/export page (upload + download) and first-run setup choices (start empty / load template / custom), with `demo-msp` + `minimal` templates. ## Acceptance criteria (this PR) - Admin can export current data to a canonical file with secrets redacted (no hashes/tokens/Stripe secrets - structural, the schema has no such fields): `GET /v1/admin/seed/export`. - Admin can import a canonical file via the shared loader: `POST /v1/admin/seed/import`. - Round trip (export -> import) reproduces the exported state: proven by `canonical_format_round_trips` at the format level; DB round-trip runs against a dev database. - Import/export are admin-only; import is additionally prod-guarded. Still open (part 2): the admin UI and first-run setup choices. ## Verification `just check-container` green (fmt + clippy `-D warnings` + `cargo test --workspace --all-targets`; seed lib 54 tests incl. the round-trip). Endpoints + export mapper are compile- and clippy-checked; DB behavior runs against a dev Postgres (the container check has none). #PSA-52
feat(seed): admin import/export API for seed data (PSA-52 part 1)
Some checks failed
E2E / Playwright against deployment (pull_request) Successful in 25s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
d926c6b425
The API layer of the in-app import/export capability. Makes the canonical seed format round-trippable and exposes admin-gated import and export endpoints; the web admin UI and first-run setup choices land in part 2.

- The seed schema now derives Serialize + PartialEq, so a SeedFile can be written back out, and a `canonical_format_round_trips` test proves parse -> serialize -> parse reproduces it (the guarantee behind "export then import reproduces state").
- `seed::export` serializes the current seed-owned data back into a SeedFile: users and feedback under the reserved domain (via new `UserRepository::list_seed_users` / `FeedbackRepository::list_seed_by_domain`, same suffix scope as reset), plus the full catalog of groups/applications. Password hashes are never emitted (the schema has no field for them), so a re-import assigns default_password and exported accounts should be treated as needing a reset.
- `GET /v1/admin/seed/export` returns the file as a JSON download; `POST /v1/admin/seed/import` loads a posted file through the shared loader. Both AdminUser-gated; import additionally runs seed_guard so demo data can never be written into a production (or unset) environment, even by an admin.

Scoped to seed-owned data (reserved domain) so a round trip re-imports cleanly; general customer-data import/export (arbitrary domains, password sourcing) depends on the PSA-56 `owns`/password_env generalizations.

Verified: just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; seed lib 54 tests incl. the round-trip). The endpoints + export mapper are compile- and clippy-checked; their DB behavior runs against a dev database (the workspace/container check has no Postgres).

#PSA-52
feat(admin-web): seed data import/export page (PSA-52 part 2)
Some checks failed
E2E / Playwright against deployment (pull_request) Successful in 37s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
47b0a13032
The in-app admin UI on top of part 1's API. Adds an admin "Seed Data" page (sidebar entry) with export and import.

- Export: a Download button streams the API's GET /v1/admin/seed/export straight to the browser as seed-export.json, reusing the feedback_export download-proxy pattern (attachment hardening included).
- Import: a paste-a-JSON textarea. The handler validates the text is JSON, forwards it to POST /v1/admin/seed/import via the shared loader, and reports the section counts (or the error) back on the page. The API keeps import non-production-only, so pasting into a prod instance returns the guard message.
- api client `import_seed` + `ImportSummary` type; export needs no client method (direct get_stream).

Verified: just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; web bin compiles + 84 web tests pass).

Deferred to a follow-up (part 3): first-run setup choices (start empty / load template / custom upload) and a template library (demo-msp / minimal). demo-msp lives in PSA-51 (#343); wiring a "load a named template" action is cleaner once that template ships, so the UI can offer it directly instead of requiring a paste.

#PSA-52
fix(seed): raise the import endpoint's payload cap above the web form limit
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 23s
Check / fmt + clippy + build + tests (pull_request) Successful in 17m59s
Create release / Create release from merged PR (pull_request) Has been skipped
624764b3dc
Review follow-up. POST /v1/admin/seed/import reads the body via actix's String extractor, whose default PayloadConfig cap is 256 KiB - below the web BFF's 2 MiB form limit. A seed file between those sizes passed the web layer but was rejected at the API with an opaque 413. Attach a route-scoped web::PayloadConfig of 4 MiB to the import resource so the API cap sits above the web form limit; the API is no longer the surprise constraint, and there is headroom for the larger files the future customer-data import (PSA-56) will carry. demo-msp is ~18 KiB, so no current file was affected.

Verified: just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets).

#PSA-52
nrupard scheduled this pull request to auto merge when all checks succeed 2026-07-06 17:29:19 +02:00
nrupard deleted branch feat/PSA-52-admin-import-export 2026-07-06 17:41:42 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!344
No description provided.