Files
hackerspace.sh/scripts/056_drop_stale_signup_trigger.sql
T
Moheeb Zara 7b8d2ae167 feat(tenant): setup CLI, CD glue, bundled compose, docs + security fix
Tier 2 productization for single-tenant instances, plus fixes from an
adversarial review pass.

New:
- scripts/setup.mjs: 'pnpm setup' CLI (doctor/provision/create-admin/all),
  idempotent, creates the first admin via the Supabase Admin API (email_confirm),
  flag > env > .env.local precedence, masked interactive prompts, --yes/--force.
- scripts/apply-migrations.sh + deploy/deploy.sh: reproducible, idempotent CD
  glue (previously only on the prod droplet), tracked via public._migrations_applied.
- docker-compose.full.yml + .env.full.example: one-command app + self-hosted
  Supabase bundle (compose config validated).
- docs/SINGLE_TENANT.md (deploy + CLI guide) and docs/CUSTOMIZE.md (the
  documented lib/tenant.ts config pattern for white-labeling); README pointer.
- __tests__/tenant.test.ts: 19 unit tests for resolveTenantConfig + isMarketingPath.

Security (found by review, pre-existing):
- Drop the stale handle_space_signup() auth trigger, which derived member role
  and space from client-controlled raw_user_meta_data and could self-insert an
  admin. Membership is created only by the vetted server actions.
  Migration scripts/056_drop_stale_signup_trigger.sql + folded into schema.sql.

Fixes:
- setup.mjs masked-password prompt hung (callback passed to readline/promises
  question); use the awaited form.
- notifications-enqueue.test: base-URL default is now localhost (platform-domain
  fallback removed), not hackerspace.sh.
- docs accuracy (logo/BrandMark, apply-migrations does not restart PostgREST, --yes).

Gates: tsc 0, lint 0 errors, 664 unit tests, build ok.
2026-08-05 13:02:18 -07:00

25 lines
1.4 KiB
SQL

-- 056_drop_stale_signup_trigger.sql
-- =============================================================================
-- SECURITY: drop the stale, unsafe auth-signup trigger.
--
-- public.handle_space_signup() (AFTER INSERT ON auth.users, SECURITY DEFINER)
-- read the new member's role and target space from raw_user_meta_data, which is
-- fully client-controlled (the browser calls supabase.auth.signUp with the anon
-- key and GoTrue stores options.data verbatim). A crafted signup with
-- data: { role: 'admin', space_id: '<known-space-uuid>' }
-- would self-insert an admin space_members row (status 'unverified', approved
-- true) with no invite, bypassing createSpace/joinSpace and their guards. Once an
-- admin approves the pending member (which flips status to 'current' without
-- resetting role) the attacker becomes a full admin.
--
-- The legitimate signup flow never puts space_id/role in auth metadata:
-- membership is created only by the createSpace / joinSpace server actions in
-- lib/auth-actions.ts. The trigger was therefore stale as well as exploitable.
-- Drop it outright rather than trying to sanitize client metadata.
--
-- Idempotent. Folded into scripts/schema.sql for fresh installs.
-- =============================================================================
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
DROP FUNCTION IF EXISTS public.handle_space_signup();