Skip to content

Phase 4: Multi-user — admin role, account self-service, per-user provider keys #36

@mforce

Description

@mforce

Tracking issue for Phase 4 of the roadmap.

Motivation

Opt-in self-registration shipped in PR #33 (Collectify:Auth:AllowRegistration), and every entity already has an OwnerId column (design hook from Phase 1), so row-level isolation is already working. The remaining multi-user work is the session / account / admin layer that turns "users can register" into "this install can actually be shared."

Today:

  • Every authenticated user can register if the flag is on, but there's no admin role. The first-run setup user is no different from a self-registered user.
  • No way to change a password without dropping into the DB.
  • No way to deactivate / remove a stale user.
  • All users share the install-wide TMDB / MusicBrainz / IGDB credentials. There's no way to BYOK.

Scope

Admin role + admin pages

  • AppUser.IsAdmin: bool (default false). The user created by the first-run /setup flow is automatically marked admin.
  • /api/auth/me exposes isAdmin so the SPA can gate the admin link in the nav.
  • New admin endpoints under /api/admin/... guarded by an AdminOnly policy:
    • GET /api/admin/users — list users { id, userName, isAdmin, isLocked, createdAt }.
    • POST /api/admin/users/{id}/lock + /unlock — toggle LockoutEnabled / LockoutEnd so the user can't sign in.
    • POST /api/admin/users/{id}/role — promote / demote (refuse demoting the last admin so the install can't lock itself out).
    • DELETE /api/admin/users/{id} — refuse if they own any rows; force the admin to transfer or hard-delete content first (no orphan rows, no surprise data loss).
  • New /admin/users page (client). Simple list + action buttons; toast on each outcome.

Self-service account

  • POST /api/auth/change-password — requires the current password, re-signs in on success.
  • /account page (client) — current username (read-only), change-password form. Per-user provider keys (below) live here too.

Per-user metadata provider keys (BYOK)

  • New UserSettings table (or columns on AppUser): TmdbApiKey, MusicBrainzUserAgent, IgdbClientId, IgdbClientSecret. GiantBomb columns can be included for completeness even though the API stays Cloudflare-gated.
  • GET /api/me/settings + PUT /api/me/settings.
  • Resolve provider options per-request: the current user's override beats MetadataLookupOptions. The providers' existing IsConfigured short-circuit still applies (e.g. no key → configured: false in the lookup response).
  • UI on /account: collapsed-by-default "Provider keys" section with one field per provider; help-text linking to where each key comes from.

Out of scope (file separately if needed)

  • OAuth providers (Google / GitHub sign-in).
  • 2FA / WebAuthn.
  • Email verification — requires SMTP infra. Registration today trusts whatever username comes in.
  • Password reset via email — same SMTP dependency.
  • Audit logging of admin actions (user X locked user Y at …).
  • Bulk OwnerId transfer on user delete (we just refuse the delete for now).

Verification

  • Default install with one user: /admin/users is reachable, list shows just that user, marked admin, with no destructive actions enabled on the self row.
  • Enable registration, register a second user. As admin, lock them → they can't sign in. Unlock → they can.
  • Promote the second user to admin; demote the first. The first now sees the admin link disappear after a refresh.
  • Try demoting the last admin → 400 with a clear error.
  • Delete a user that owns rows → 400 with a clear error.
  • Self-service: change own password, sign in with the new one.
  • Drop a TMDB key into /account settings. Without changing the install-wide config, a fresh TMDB search uses the user-supplied key (verify by setting the install-wide key to a known-bad value and confirming the lookup still works for that user).

Implementation notes

  • Identity already supports the lockout fields (LockoutEnabled / LockoutEnd) — we just need to wire admin endpoints to toggle them. Don't roll our own user-state column.
  • An AdminOnly AuthorizationPolicy + .RequireAuthorization("AdminOnly") on the admin endpoint group is the right shape.
  • Backfill: existing single-user install gets IsAdmin = true on the existing row at migration time so they don't lock themselves out of their own install.
  • For BYOK, the cleanest plumbing is a per-request scoped wrapper around IOptions<MetadataLookupOptions> that overlays the current user's settings on top. The providers don't need to know about the multi-user shape; they keep reading from IOptions<MetadataLookupOptions>.Value.

Phasing

The three subsections (admin pages / self-service / BYOK) are independent enough that they can land as separate PRs if the scope feels too big to ship in one slice. Suggested order:

  1. Admin role + admin pages (the table-stakes piece).
  2. Self-service change-password (small, ships fast).
  3. BYOK (largest of the three; touches every metadata provider's options plumbing).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions