6 Commits
Author SHA1 Message Date
aarbitandClaude Sonnet 5 6cdfde822d Add site-wide announcements, account migration, and profile self-service
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
2026-09-28 10:05:11 -05:00
aarbit 4b757e89f5 Prevent sharing a diagram with yourself
RLS is the real guard (diagram_collaborators_insert/update now reject
user_id = the diagram's owner, regardless of who's performing the write —
covers a Super Admin acting on someone else's diagram too, not just the
normal owner path); the client-side check in
SupabaseDiagramCollaboratorRepository.add is just there to surface a
friendly message instead of the raw 42501.

Verified: tsc -b and oxlint clean; supabase db reset + 53/53 pgTAP tests
pass (1 new test).
2026-09-11 11:42:07 -05:00
aarbit dea26f7ee8 Add diagram sharing/collaborators, version history, and view-only lockdown
Per organized-ideas.md §8. Backend tables/RLS (diagrams, diagram_collaborators,
diagram_snapshots) already existed from an earlier phase — this is the
frontend for them, plus two small backend additions.

Backend (supabase/migrations/20260913000000_diagram_sharing.sql):
- find_user_id_by_username(text): lets any authenticated user resolve a
  username to an id for "share with @username" — unlike general profile
  browsing (blocked by profiles_select_self_or_super_admin), a username is
  meant to be a shareable handle, so this is deliberately not gated.
- diagram_collaborator_usernames / diagram_snapshot_saved_by_usernames:
  same pattern as the admin-review-queue phase's submitter-username
  lookup — batched per diagram, gated to "can you see this diagram at all"
  (reusing diagrams_select's own helper functions).
- prune_diagram_snapshots trigger: keeps the 50 most recent snapshots per
  diagram, enforced at write time rather than a scheduled job (diagram_
  snapshots has no update/delete policy for regular users at all).
- 14 new pgTAP tests (52/52 total).

Frontend:
- DiagramCollaboratorRepository/store + DiagramSharingModal: add/remove
  collaborators by username, per-person view/edit permission, owner-only
  controls.
- DiagramSnapshotRepository/store + VersionHistoryModal (its own top-bar
  button, not nested under Share — moved there after review): periodic
  checkpoints (one per 5 min of active editing) written as a side effect
  of normal saves, list + restore.
- Restore's duplicate-snapshot problem: repeatedly jumping between old
  versions without editing in between was writing a near-duplicate safety
  snapshot on every jump. Fixed by having projectStore track which
  snapshot the diagram was last restored from and its updatedAt at that
  moment (touch() always advances updatedAt on a genuine edit) — a restore
  skips the safety snapshot when nothing has changed since the last one,
  and the tracking clears on any real edit so in-progress work stays
  protected.
- DiagramRepository gains getAccess() (owner id + your own permission for
  the open diagram) — surfaced in projectStore as `access`.
- View-only enforcement: FlowCanvas disables drag/connect/drop
  (nodesDraggable/nodesConnectable + guarded handlers), DeviceInspector/
  ConnectionInspector wrap their controls in a disabled <fieldset>,
  DevicePalette disables adding devices to the canvas, TopBar disables the
  rename field, and a ViewOnlyBanner makes the restriction visible instead
  of leaving a collaborator to discover it as controls that just don't
  work. Autosave itself also refuses to write for a view-only user, as a
  backstop behind the UI-level lockdown.

Verified: tsc -b and oxlint clean; supabase db reset + 52/52 pgTAP tests
pass; confirmed find_user_id_by_username works through the real REST API
via a live curl call (signup, confirm, resolve). Manually tested two-
account sharing (view vs. edit), restoring history, and the duplicate-
snapshot fix.
2026-09-11 11:28:58 -05:00
aarbit 4c45b5afa7 Add Roles & Admin/Super-Admin interface
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.
2026-09-08 16:01:01 -05:00
aarbit 1f8d49345e Add the Admin review queue
Lets an Admin/Super-Admin review pending catalog submissions and approve
(in place, same id) or reject (with a required reason) them, per
organized-ideas.md §3/§9.

Backend (supabase/migrations/20260910010000_admin_review_queue.sql):
- Per-user pending-submission cap (10), enforced in catalog_submissions'
  insert policy rather than trusted to the client.
- catalog_entity_usage_impact(entity_type, entity_id): a SECURITY DEFINER,
  admin-gated aggregate function answering "how many diagrams reference
  this, and a short sample" by scanning diagrams.data JSONB — never raw
  diagram content, and available to regular Admins even though they don't
  otherwise have diagram visibility (only Super Admins do, per §6).
- catalog_submission_submitters(ids[]): same admin-gated pattern, batched,
  so the queue can show who submitted something without opening general
  profile browsing to regular Admins.
- Follow-up migration: a rejected submission had no way out (the delete
  policy only allowed withdrawing 'pending') — extended to allow 'rejected'
  too, so a submitter can dismiss one they don't intend to revise.
- 12 new pgTAP tests (38/38 total) covering the cap, both privileged
  functions (including the non-admin-gets-rejected case), and withdrawing
  pending vs. rejected submissions.

Frontend:
- authStore: minimal role awareness, replacing TopBar's local username
  fetch, used to gate the Review Queue UI.
- AdminSubmissionRepository/SupabaseAdminSubmissionRepository +
  adminReviewStore: list all submissions, approve/reject, usage impact,
  submitter usernames.
- AdminReviewModal: per-submission diff view (current vs. proposed, both
  row-shaped via the existing catalog<->row mappers), a duplicate-detection
  nudge (Levenshtein distance against existing public device names) for
  new device submissions, and an inline impact-check for edits to
  already-public entries before approving.
- TopBar: role-gated "Review Queue" button with a pending-count badge; "My
  Submissions" gets an unseen-outcome badge (localStorage-tracked, like the
  existing hidden-template preference) so a submitter notices a decision
  without having to keep reopening the modal.
- Deliberately deferred: the site-wide announcement banner (its own
  follow-up, per discussion) and the Admin/Super-Admin role-assignment UI
  (§9's later phase — becoming an Admin locally still means setting
  profiles.role via SQL/Studio).

Verified: tsc -b and oxlint clean; supabase db reset + 38/38 pgTAP tests
pass; confirmed the two new RPC functions are actually reachable through
PostgREST (not just raw SQL) via a live curl call; manually tested
submit -> review -> approve/reject -> (for rejected) dismiss end to end.
2026-09-08 13:01:56 -05:00
aarbitandClaude Sonnet 5 8cea3f8b92 Add local Supabase backend foundation: schema, RLS, and RLS tests
- 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
2026-09-04 23:11:52 -05:00