feat(fase-1): auth, collective management, and DB migrations
- SQL migrations: users, collectives, collective_members, collective_invitations,
full RLS policies, is_active_member/is_member/is_admin helpers,
accept_invitation SECURITY DEFINER function, admin auto-promote trigger,
avatars storage bucket
- Auth refactor: replace keycloak-js with Supabase OAuth (GoTrue OIDC proxy,
Option B). signInWithOAuth({ provider: 'keycloak' }) + PKCE flow
- GoTrue config: GOTRUE_EXTERNAL_KEYCLOAK_URL + GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI
added to docker-compose.dev.yml; Keycloak realm updated with GoTrue callback URI
- New routes: /auth/callback, /invitation/[token], /(app)/collective/manage
- Functional onboarding: create collective or join via invite link
- Full settings page: display name (debounced), avatar (initials/emoji/upload),
language switcher, Keycloak account console link
- Components: Avatar.svelte (initials/emoji/upload with fallback),
ImageCropper.svelte (cropperjs, 1:1 crop, lazy-loaded)
- i18n: added all Fase 1 message keys (en + es); renamed 'delete' → 'action_delete'
(JS reserved word); fixed plugin-m-function-matcher major-version URL format
- Database types: manually seeded packages/types/src/database.ts from migrations
- .env.development: committed public dev defaults for SvelteKit type checking
- CLAUDE.md: documented /etc/hosts requirement for keycloak hostname
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
87
supabase/migrations/001_users.sql
Normal file
87
supabase/migrations/001_users.sql
Normal file
@@ -0,0 +1,87 @@
|
||||
-- Migration 001: users table
|
||||
-- Mirrors GoTrue auth.users — populated by trigger on first OIDC login.
|
||||
|
||||
-- ── Enums ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TYPE public.language_code AS ENUM ('en', 'es');
|
||||
CREATE TYPE public.avatar_type AS ENUM ('initials', 'emoji', 'upload');
|
||||
|
||||
-- ── Table ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE public.users (
|
||||
id uuid PRIMARY KEY, -- matches auth.users.id (GoTrue UUID)
|
||||
email text NOT NULL,
|
||||
display_name text NOT NULL DEFAULT '',
|
||||
language public.language_code NOT NULL DEFAULT 'en',
|
||||
avatar_type public.avatar_type NOT NULL DEFAULT 'initials',
|
||||
avatar_emoji text NULL,
|
||||
avatar_url text NULL, -- signed URL from Supabase Storage
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ── updated_at trigger ────────────────────────────────────────────────────────
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.set_updated_at()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER users_set_updated_at
|
||||
BEFORE UPDATE ON public.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
-- ── Sync trigger: GoTrue auth.users → public.users on first OIDC login ────────
|
||||
-- GoTrue inserts into auth.users after a successful Keycloak OIDC exchange.
|
||||
-- We mirror display_name and detect language from raw_user_meta_data claims.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_display_name text;
|
||||
v_language public.language_code;
|
||||
v_raw jsonb;
|
||||
BEGIN
|
||||
v_raw := NEW.raw_user_meta_data;
|
||||
|
||||
-- Build display name: full_name > name > email prefix
|
||||
v_display_name := COALESCE(
|
||||
NULLIF(trim(v_raw->>'full_name'), ''),
|
||||
NULLIF(trim(v_raw->>'name'), ''),
|
||||
split_part(NEW.email, '@', 1)
|
||||
);
|
||||
|
||||
-- Detect language from OIDC locale claim; default en
|
||||
v_language := CASE
|
||||
WHEN v_raw->>'locale' ILIKE 'es%' THEN 'es'::public.language_code
|
||||
ELSE 'en'::public.language_code
|
||||
END;
|
||||
|
||||
INSERT INTO public.users (id, email, display_name, language)
|
||||
VALUES (NEW.id, NEW.email, v_display_name, v_language)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET email = EXCLUDED.email,
|
||||
-- Preserve user-set display name; only backfill if still empty
|
||||
display_name = CASE
|
||||
WHEN public.users.display_name = '' THEN EXCLUDED.display_name
|
||||
ELSE public.users.display_name
|
||||
END;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
||||
Reference in New Issue
Block a user