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.
This commit is contained in:
+14
-47
@@ -634,54 +634,21 @@ CREATE TRIGGER on_space_created
|
||||
AFTER INSERT ON public.spaces
|
||||
FOR EACH ROW EXECUTE FUNCTION public.create_default_channels();
|
||||
|
||||
-- Auth signup hook
|
||||
-- Called when a new user is created in auth.users.
|
||||
-- Reads metadata from supabase.auth.signUp({ data: { space_id, role, display_name } })
|
||||
-- and inserts the corresponding space_members row.
|
||||
CREATE OR REPLACE FUNCTION public.handle_space_signup()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_space_id uuid;
|
||||
v_role public.member_role;
|
||||
v_display_name text;
|
||||
BEGIN
|
||||
v_space_id := (NEW.raw_user_meta_data->>'space_id')::uuid;
|
||||
|
||||
-- Default to 'member' if role is missing or invalid
|
||||
v_role := COALESCE(
|
||||
NULLIF(NEW.raw_user_meta_data->>'role', '')::public.member_role,
|
||||
'member'
|
||||
);
|
||||
|
||||
-- Prefer display_name, fall back to full_name, then email
|
||||
v_display_name := COALESCE(
|
||||
NULLIF(NEW.raw_user_meta_data->>'display_name', ''),
|
||||
NULLIF(NEW.raw_user_meta_data->>'full_name', ''),
|
||||
NEW.email
|
||||
);
|
||||
|
||||
-- No space_id means this is not a hackerspace signup flow
|
||||
IF v_space_id IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.space_members
|
||||
(space_id, user_id, role, status, display_name, email, approved)
|
||||
VALUES
|
||||
(v_space_id, NEW.id, v_role, 'unverified', v_display_name, NEW.email, true)
|
||||
ON CONFLICT (space_id, user_id) DO NOTHING;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Auth signup hook — REMOVED (was a privilege-escalation vector). See
|
||||
-- scripts/056_drop_stale_signup_trigger.sql for the incremental drop on
|
||||
-- existing databases.
|
||||
--
|
||||
-- The old handle_space_signup() trigger derived the new member's role and space
|
||||
-- from auth.users.raw_user_meta_data, which is fully client-controlled: the
|
||||
-- browser calls supabase.auth.signUp({ options: { data: {...} } }) with the anon
|
||||
-- key and GoTrue stores that verbatim. A crafted signup with
|
||||
-- data:{ role:'admin', space_id:'<known-uuid>' } would self-insert an admin
|
||||
-- space_members row (approved=true), bypassing the createSpace/joinSpace guards
|
||||
-- entirely. The legitimate signup flow never sets space_id/role in metadata
|
||||
-- (membership is created by the vetted server actions in lib/auth-actions.ts),
|
||||
-- so the trigger was stale as well as dangerous. Dropped, not fixed.
|
||||
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
||||
CREATE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_space_signup();
|
||||
DROP FUNCTION IF EXISTS public.handle_space_signup();
|
||||
|
||||
-- Privilege-escalation guard.
|
||||
-- RLS lets a member update their own space_members row (so they can fix their
|
||||
|
||||
Reference in New Issue
Block a user