Files
WishNinja/docs/DESIGN.md
T
Jim BassoandClaude Opus 4.8 830eb4f6dc
Build & Push Docker image / test (push) Successful in 45s
Build & Push Docker image / docker (push) Successful in 1m15s
Add TODO.md backlog and docs/DESIGN.md (original plan)
Track the project backlog in-repo and preserve the original pre-implementation
design with a note on where the build diverged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 15:50:18 -05:00

5.9 KiB

WishNinja — Design (original plan)

Status: This is the original pre-implementation design, kept for posterity. The build followed it closely, but a few details evolved during implementation. For the current state of the code, the README and the git history are authoritative. Notable deltas from this document:

  • Item images are always stored locally. Pasted URLs are downloaded server-side and saved as files; the ImageKind.ExternalUrl value and WishlistItem.ImageUrl column were removed.
  • Ordering: list queries order by Id (not CreatedAt), because SQLite cannot ORDER BY a DateTimeOffset column.
  • Connection string: derived from WishNinja__DataPath (default /data); override via ConnectionStrings__DefaultConnection (not ConnectionStrings__Default).
  • CI registry auth: uses a REGISTRY_TOKEN PAT secret, not the auto-provided token.
  • Seeder: implemented as Services/StartupInitializer.cs (migrate + seed roles + admin), not AdminSeeder.cs.
  • Container runs as root for hassle-free Unraid bind-mount permissions (the "non-root" note below was not adopted).

Context

The user runs an Unraid home server hosting many self-hosted apps and wants a new gift wishlist manager ("WishNinja") that ships as a Docker image and lives in their self-hosted Gitea instance. The working directory was empty — a clean greenfield build.

The app lets a closed group (family/friends) create wishlists, add items with links/prices/ images, and claim/reserve gifts so the owner never sees what's been claimed (preserving the surprise) while other viewers do (avoiding duplicate gifts). Users are managed internally with email-based password resets.

Confirmed decisions

  • Stack: ASP.NET Core Blazor Web App (interactive server render mode), .NET 10 LTS.
  • Persistence: SQLite via EF Core, single DB file on a mounted volume.
  • Auth: ASP.NET Core Identity with internal accounts + email password resets.
  • Onboarding: Admin invites only (no open self-registration).
  • Claim privacy: Claims hidden from the list owner; visible to other viewers.
  • Item images: file upload + clipboard paste (both stored locally on /data) + external URL.
  • Delivery: Docker image, built via Gitea Actions → Gitea container registry, run on Unraid.

Architecture & Stack

Concern Choice
Framework ASP.NET Core 10 (LTS), Blazor Web App, InteractiveServer render mode
ORM / DB EF Core 10 + SQLite (Microsoft.EntityFrameworkCore.Sqlite)
Identity ASP.NET Core Identity (cookie auth, roles: Admin, User)
Email MailKit for SMTP (password reset + invite emails)
Data Protection Keys persisted to /data/keys so cookies/antiforgery survive restarts
Image storage Local files under /data/uploads; served via a static-files mapping
Container base mcr.microsoft.com/dotnet/aspnet:10.0 (Debian slim), built from sdk:10.0

Why Blazor Server interactivity: all-C#, single container, no separate JS build, live claim buttons. Reverse proxy must allow WebSocket upgrades (SignalR) — documented in the README.


Data Model (EF Core entities)

  • ApplicationUser : IdentityUser — adds DisplayName, CreatedAt.
  • WishlistId, OwnerId (FK user), Title, Description, Visibility (AllMembers | SpecificUsers), IsArchived, CreatedAt.
  • WishlistShareWishlistId, UserId (rows only when Visibility = SpecificUsers).
  • WishlistItemId, WishlistId, Name, Description, ProductUrl, ImageKind, ImagePath, Price, Priority, Quantity, SortOrder, CreatedAt.
  • ClaimId, WishlistItemId, ClaimedByUserId, Quantity, Note, CreatedAt.
  • InviteId, Email, TokenHash, Role, InvitedByUserId, ExpiresAt, AcceptedAt.

EF migrations applied automatically at startup for friction-free self-hosting.


Core Features

Wishlists & items

  • CRUD wishlists; set visibility (all members / specific users via WishlistShare).
  • CRUD items with name, description, product link, price, priority, quantity, image.
  • Item images via three inputs, all stored locally:
    • File upload → saved to /data/uploads, ImageKind=Uploaded.
    • Clipboard paste → JS-interop paste handler captures the blob, uploads via the same path.
    • Image URL → downloaded server-side and saved as a local file.
    • Upload validation: allowlist content types (png/jpeg/webp/gif), size cap, GUID filenames.

Claim / reservation (owner-hidden)

  • Any member viewing a list they don't own sees each item's claim state and can claim/ unclaim (respecting Quantity).
  • The owner's view of their own list never loads or renders claim data — enforced at the query layer, not just hidden in the UI, so it can't leak via timing/DOM. This is the single most important correctness rule and gets dedicated unit tests.

Auth & onboarding (admin-invite-only)

  • Self-registration disabled. Admin invites by email → emailed invite link with a one-time token → recipient sets DisplayName + password → account created with User role.
  • First-run admin seeding from env vars (SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD).
  • Password reset via Identity's token flow + MailKit SMTP email.
  • Admin area: list/disable users, manage invites.

Verification

  • Automated: dotnet test — owner queries never return claim data; non-owner queries do; invite token single-use/expiry; image upload type/size validation; list queries run on SQLite.
  • Container: docker compose up --build; verify SQLite DB + uploads persist across restarts via the /data volume.
  • Unraid: deploy image, map appdata volume + port, place behind reverse proxy with WebSockets enabled, confirm Blazor interactivity (claim button) works through the proxy.