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.
25 lines
1.4 KiB
SQL
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();
|