Announcements: a Super Admin (or an Admin individually flagged via
profiles.can_post_announcements) can post/retire a site-wide banner.
Account migration: a Super Admin can move a locked-out user's diagrams,
private catalog entries, and submissions to another account, with a
migration-history log; ProfileModal adds the self-service half (link a new
Google identity via Supabase manual linking, then unlink the old one,
while signed in as the account being migrated). Also reworks the top bar's
flat button row into grouped dropdown menus (Diagram / Admin / Account) now
that there are enough entries to need it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
Per organized-ideas.md §6: role assignment, account ban/unban/delete, and
direct Admin/Super-Admin CRUD of public catalog entries outside the
submission workflow.
Backend:
- list_users_for_admin(): Super-Admin-gated SECURITY DEFINER function
joining profiles + auth.users (username, email, role, banned_until) —
auth.users isn't exposed through PostgREST, so this is the only way to
list accounts at all.
- New Edge Function admin-user-action (ban/unban/delete), using
@supabase/server's `auth: 'user'` mode to verify the caller's JWT, then
Supabase Auth's Admin API for the actual mutation. This is deliberately
an Edge Function rather than a Postgres function like everything else in
this codebase: touching auth.users needs the Admin API, the stable
documented interface, not a direct write to a schema Supabase manages
internally. Self-action guard; verify_jwt = true at the gateway on top of
the function's own JWT verification.
- 5 new pgTAP tests (43/43 total) for list_users_for_admin (Super-Admin-only,
even regular Admins get 42501).
- CatalogRepository gains admin* methods (direct edit of a public port/cable/
device entry, plus adminUnpublish which flips is_public rather than
deleting) — the update methods were already ownership-agnostic (RLS's
is_admin() clause is what actually permits it), so these are thin aliases,
not duplicated logic.
Frontend:
- authStore/AdminUserRepository: minimal role plumbing, shared UserRole type.
- adminUserStore + AdminUsersModal: list/role-dropdown/ban/unban/delete,
gated to Super Admin only via a new "Manage Users" TopBar button.
- PortTypeManager/CableTypeManager/DevicePalette: built-in entries now show
direct "Edit"/"Unpublish" for Admins (regular Admin included, per §6's
capability table — not Super-Admin-exclusive) instead of "Suggest edit";
unpublish reuses the review-queue's impact-check RPC before confirming.
- DeviceTemplateEditor gains an `adminMode` save path alongside its existing
submissionMode/resubmitId ones.
Verified: tsc -b and oxlint clean; supabase db reset + 43/43 pgTAP tests
pass; confirmed both new privileged endpoints (the SQL function and the
Edge Function) actually work through the real REST API via live curl
calls — signup, email confirm, role promotion, ban/unban/delete round
trips, self-action guard, non-super-admin rejection, and verify_jwt=true
compatibility all exercised directly, not just asserted.
- Migration: profiles.username is now nullable -- Google's OAuth
redirect can't collect a username up front the way the email/password
signup form does, so a first-time Google sign-in's profile is created
with no username.
- supabase/config.toml: [auth.external.google] enabled, credentials via
env() substitution (SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID/_SECRET
in .env.local, which the CLI auto-loads). skip_nonce_check is on,
which Supabase's own docs call out as required for local sign-in.
- LoginScreen: "Continue with Google" alongside the existing
email/password form.
- CompleteProfileScreen: one-time gate for a signed-in user with no
username yet (i.e. first Google sign-in) -- same hard-gate spirit as
email verification, nothing else is usable until a username is set.
- App.tsx now checks profiles.username after establishing a session and
routes to CompleteProfileScreen before AppShell when it's missing.
RLS test suite re-run clean (23/23) after the schema change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
- supabase/config.toml: local dev stack config, pinned to the app's
fixed dev server port, email confirmation required (hard
verification gate per organized-ideas.md).
- Initial schema migration: profiles/roles, the public/private
catalog tables (manufacturers, device categories, port types, cable
types, device templates + ports) with the shared is_public/owner_id
RLS pattern, a generalized catalog_submissions review-queue table,
and diagrams as JSONB documents (+ collaborators, snapshots) rather
than fully normalized -- see the migration's header comment for why.
- pgTAP RLS test suite (23 assertions) covering catalog visibility and
promotion-in-place, diagram owner/collaborator/admin/super-admin
visibility and edit permissions, submission visibility, and role
escalation. Caught and fixed a real infinite-recursion bug between
the diagrams and diagram_collaborators policies before this ever
touched real data.
- vite.config.ts: pinned dev server port so Supabase Auth's redirect
allow-list doesn't silently break if Vite floats to another port.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x