Files
Jim BassoandClaude Opus 4.8 bc025d901b
Build & Push Docker image / test (push) Successful in 49s
Build & Push Docker image / docker (push) Successful in 1m18s
Docs: document Gmail SMTP setup for email
Add a Gmail App Password walkthrough to the README and note in CLAUDE.md that
production email is configured, including the Unraid-template persistence caveat.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 17:02:33 -05:00

6.3 KiB

🥷 WishNinja

A self-hosted gift wishlist manager for a closed group (family/friends). Create wishlists, add items with links, prices and images, and let others claim gifts so the owner never sees what's been claimed — preserving the surprise while preventing duplicate gifts.

Built with ASP.NET Core Blazor (.NET 10), EF Core + SQLite, and ASP.NET Core Identity. Ships as a single Docker image; all state lives in one mounted volume.

Features

  • Wishlists & items — title/description, per-item link, price, priority, quantity and image.
  • Three ways to add an image — upload a file, fetch from an image URL, or just Ctrl+V a copied image. All images are stored locally on the data volume; pasted URLs are downloaded server-side and saved as files, so a wishlist never breaks when the source URL goes away.
  • Owner-hidden claims — when someone claims/reserves an item, the list owner can never see it. Other viewers see claims (and remaining quantity) to avoid double-gifting. Enforced at the query layer, not just hidden in the UI.
  • Sharing — a list is visible to all members or only to specific people you choose.
  • Invite-only accounts — no open registration. Admins invite by email; the recipient follows a single-use link to set their display name + password. Password resets via email.
  • Admin area — manage users (promote/disable) and invitations.

Quick start (Docker Compose)

docker compose up --build

Then open http://localhost:8080, and sign in with the bootstrap admin credentials from docker-compose.yml (SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD). Change these before first run.

If SMTP isn't configured, emails aren't sent — but invite links are still shown directly in the admin Invitations page, so you can onboard users without an email server.

Configuration

All settings are environment variables (double-underscore maps to nested config).

Variable Purpose Default
WishNinja__DataPath Directory for the SQLite db, uploads and keys /data (in container)
WishNinja__BaseUrl Public URL, used in email links https://localhost:7777
SEED_ADMIN_EMAIL First-run bootstrap admin email
SEED_ADMIN_PASSWORD First-run bootstrap admin password
WishNinja__Smtp__Host SMTP server (empty ⇒ emails logged, not sent) empty
WishNinja__Smtp__Port SMTP port 587
WishNinja__Smtp__UseStartTls Use STARTTLS true
WishNinja__Smtp__User / __Password SMTP credentials empty
WishNinja__Smtp__FromAddress / __FromName Sender identity
WishNinja__Uploads__MaxBytes Max upload size in bytes 5242880 (5 MB)
WishNinja__Invites__ExpiryHours Invite link lifetime 168 (7 days)
ConnectionStrings__DefaultConnection Override the SQLite connection string derived from DataPath

The container listens on port 8080 over plain HTTP — terminate TLS at your reverse proxy.

Email (Gmail example)

Invites and password resets need SMTP. Gmail works via an App Password (not your account password):

  1. Enable 2-Step Verification on the Google account, then create an App Password at https://myaccount.google.com/apppasswords. You get a 16-character password — use it without spaces.
  2. Set the SMTP variables:
    Variable Value
    WishNinja__Smtp__Host smtp.gmail.com
    WishNinja__Smtp__Port 587
    WishNinja__Smtp__UseStartTls true
    WishNinja__Smtp__User your full Gmail address
    WishNinja__Smtp__Password the 16-char app password (no spaces)
    WishNinja__Smtp__FromAddress same Gmail address (Gmail requires From = authenticated user)
    WishNinja__Smtp__FromName e.g. WishNinja

Other providers work the same way — just swap Host/Port/User/Password. Leave Smtp__Host empty to disable email entirely (invite links still appear in the admin UI).

Unraid note: set these as container variables via the template so they persist across Force Update redeploys — variables added only to a running container are lost on recreate.

Running on Unraid

  1. Build & publish the image. Push to your Gitea repo; the included .gitea/workflows/build.yml runs the tests and pushes your-gitea-host/youruser/wishninja:latest to Gitea's built-in container registry. (Or build locally and push manually.)
  2. Add the container (Docker tab → Add Container, or import unraid-template.xml):
    • Repository: your-gitea-host/youruser/wishninja:latest
    • Port: host 8080 → container 8080
    • Path: host /mnt/user/appdata/wishninja → container /data
    • Env vars: at minimum SEED_ADMIN_EMAIL, SEED_ADMIN_PASSWORD, WishNinja__BaseUrl, and SMTP settings.
  3. Reverse proxy (SWAG / Nginx Proxy Manager). Blazor's interactive server mode uses SignalR over WebSockets, so the proxy must have WebSocket support enabled (NPM: the "Websockets Support" toggle; SWAG: included in the default proxy-conf samples).

Everything (database, uploaded images, data-protection keys) lives under /data, so backing up /mnt/user/appdata/wishninja backs up the whole app, and container updates preserve all state.

Development

# Run locally (uses ./Data for the SQLite db by default)
cd src/WishNinja
SEED_ADMIN_EMAIL=admin@local SEED_ADMIN_PASSWORD='Admin!2345' dotnet run

# Run the tests
dotnet test

EF Core migrations are applied automatically on startup. To add a migration after changing entities:

dotnet ef migrations add <Name> --project src/WishNinja/WishNinja.csproj --output-dir Data/Migrations

Project layout

src/WishNinja/          ASP.NET Core Blazor Web App
  Data/                 EF Core context, entities, migrations
  Services/             EmailSender, InviteService, ImageService, WishlistService, seeding
  Components/           Razor components (Pages, Account, Wishlists, Layout)
  wwwroot/js/           clipboard-paste interop
tests/WishNinja.Tests/  xUnit tests (claim privacy, invite lifecycle, image validation)
Dockerfile              multi-stage build → aspnet:10.0 runtime
docker-compose.yml      local + Unraid reference
.gitea/workflows/       CI: test + build/push image