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]>
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.ExternalUrlvalue andWishlistItem.ImageUrlcolumn were removed.- Ordering: list queries order by
Id(notCreatedAt), because SQLite cannotORDER BYaDateTimeOffsetcolumn.- Connection string: derived from
WishNinja__DataPath(default/data); override viaConnectionStrings__DefaultConnection(notConnectionStrings__Default).- CI registry auth: uses a
REGISTRY_TOKENPAT secret, not the auto-provided token.- Seeder: implemented as
Services/StartupInitializer.cs(migrate + seed roles + admin), notAdminSeeder.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) |
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 whenVisibility = 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
pastehandler 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.
- File upload → saved to
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 withUserrole. - 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/datavolume. - Unraid: deploy image, map appdata volume + port, place behind reverse proxy with WebSockets enabled, confirm Blazor interactivity (claim button) works through the proxy.