Files
collective-lists/supabase/migrations/001_users.sql
Oier Bravo Urtasun 7d91705fc2 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>
2026-04-12 15:29:29 +02:00

88 lines
3.3 KiB
PL/PgSQL

-- 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();