# 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`. - **Wishlist** — `Id`, `OwnerId` (FK user), `Title`, `Description`, `Visibility` (`AllMembers` | `SpecificUsers`), `IsArchived`, `CreatedAt`. - **WishlistShare** — `WishlistId`, `UserId` (rows only when `Visibility = SpecificUsers`). - **WishlistItem** — `Id`, `WishlistId`, `Name`, `Description`, `ProductUrl`, `ImageKind`, `ImagePath`, `Price`, `Priority`, `Quantity`, `SortOrder`, `CreatedAt`. - **Claim** — `Id`, `WishlistItemId`, `ClaimedByUserId`, `Quantity`, `Note`, `CreatedAt`. - **Invite** — `Id`, `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.