// ===================================================================== // Edge Function : admin-users // Gère les comptes (lister / créer / supprimer / modifier). // Tourne sur les serveurs Supabase et garde la clé service_role en // sécurité — elle n'est JAMAIS exposée dans l'application. // // Déploiement : // supabase functions deploy admin-users --no-verify-jwt // (la fonction vérifie elle-même l'identité de l'appelant ci-dessous) // // v10 : la colonne `profiles.role` a disparu ; seul `roles[]` fait foi. // ===================================================================== import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Methods': 'POST, OPTIONS' } const SUPABASE_URL = Deno.env.get('SUPABASE_URL')! const ANON_KEY = Deno.env.get('SUPABASE_ANON_KEY')! const SERVICE_ROLE = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! const VALID_ROLES = [ 'lead', 'programmer', 'artist', 'game_designer', 'sound', 'writer', 'qa', 'other' ] function json(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...cors, 'Content-Type': 'application/json' } }) } /** Ne garde que des pôles valides (l'enum SQL rejetterait le reste). */ function cleanRoles(input: unknown): string[] { if (!Array.isArray(input)) return [] return [...new Set(input.filter((r) => typeof r === 'string' && VALID_ROLES.includes(r)))] } Deno.serve(async (req) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: cors }) try { // --- 1) Identifier l'appelant à partir de son token de connexion --- const authHeader = req.headers.get('Authorization') ?? '' const caller = createClient(SUPABASE_URL, ANON_KEY, { global: { headers: { Authorization: authHeader } } }) const { data: { user }, error: userErr } = await caller.auth.getUser() if (userErr || !user) return json({ error: 'Non authentifié.' }, 401) // --- 2) Client « admin » (service_role) pour les opérations sensibles --- const admin = createClient(SUPABASE_URL, SERVICE_ROLE, { auth: { autoRefreshToken: false, persistSession: false } }) // --- 3) Vérifier que l'appelant est bien administrateur --- const { data: me } = await admin .from('profiles') .select('is_admin') .eq('id', user.id) .single() if (!me?.is_admin) return json({ error: 'Accès réservé aux administrateurs.' }, 403) // --- 4) Exécuter l'action demandée --- const { action, ...p } = await req.json() switch (action) { case 'list': { // Les emails sont dans auth.users, le reste dans profiles → fusion. const { data: list, error } = await admin.auth.admin.listUsers({ perPage: 1000 }) if (error) throw error const { data: profiles } = await admin.from('profiles').select('*') const byId = new Map((profiles ?? []).map((pr: Record) => [pr.id, pr])) const users = list.users .map((u) => { const pr = (byId.get(u.id) ?? {}) as Record return { id: u.id, email: u.email ?? '', full_name: (pr.full_name as string) ?? '', roles: Array.isArray(pr.roles) ? pr.roles : [], is_admin: Boolean(pr.is_admin), avatar_color: (pr.avatar_color as string) ?? '#7c5cff', avatar_url: (pr.avatar_url as string) ?? null, created_at: (pr.created_at as string) ?? u.created_at } }) .sort((a, b) => a.full_name.localeCompare(b.full_name, 'fr')) return json(users) } case 'create': { const { email, password, full_name, roles, is_admin } = p if (!email || !password) return json({ error: 'Email et mot de passe requis.' }, 400) if (String(password).length < 6) { return json({ error: 'Le mot de passe doit faire au moins 6 caractères.' }, 400) } const roleList = cleanRoles(roles) const primary = roleList[0] ?? 'other' const name = full_name || String(email).split('@')[0] // email_confirm : le membre peut se connecter immédiatement. const { data: created, error } = await admin.auth.admin.createUser({ email, password, email_confirm: true, user_metadata: { full_name: name, role: primary } }) if (error) return json({ error: error.message }, 400) // Le trigger handle_new_user a créé le profil ; on cale les champs. const { error: profErr } = await admin .from('profiles') .update({ full_name: name, roles: roleList.length ? roleList : [primary], is_admin: Boolean(is_admin) }) .eq('id', created.user.id) if (profErr) return json({ error: profErr.message }, 400) return json({ ok: true, id: created.user.id }) } case 'delete': { const { id } = p if (!id) return json({ error: 'Identifiant manquant.' }, 400) if (id === user.id) { return json({ error: 'Tu ne peux pas supprimer ton propre compte.' }, 400) } const { error } = await admin.auth.admin.deleteUser(id) // profil supprimé en cascade if (error) return json({ error: error.message }, 400) return json({ ok: true }) } case 'update': { const { id, full_name, email, password, roles, is_admin, avatar_url } = p if (!id) return json({ error: 'Identifiant manquant.' }, 400) if (id === user.id && is_admin === false) { return json({ error: 'Tu ne peux pas retirer ton propre statut admin.' }, 400) } // --- a) Compte auth (email / mot de passe / métadonnées) --- const authPatch: Record = {} if (email !== undefined && String(email).trim()) { authPatch.email = String(email).trim() authPatch.email_confirm = true // évite le mail de confirmation } if (password !== undefined && String(password) !== '') { if (String(password).length < 6) { return json({ error: 'Le mot de passe doit faire au moins 6 caractères.' }, 400) } authPatch.password = String(password) } if (full_name !== undefined) { // On fusionne avec les métadonnées existantes plutôt que de les écraser. const { data: existing } = await admin.auth.admin.getUserById(id) authPatch.user_metadata = { ...(existing?.user?.user_metadata ?? {}), full_name } } if (Object.keys(authPatch).length > 0) { const { error: authErr } = await admin.auth.admin.updateUserById(id, authPatch) if (authErr) return json({ error: authErr.message }, 400) } // --- b) Profil (nom, pôles, admin, avatar) --- const patch: Record = {} if (full_name !== undefined) patch.full_name = full_name if (is_admin !== undefined) patch.is_admin = Boolean(is_admin) if (avatar_url !== undefined) patch.avatar_url = avatar_url || null if (roles !== undefined) patch.roles = cleanRoles(roles) if (Object.keys(patch).length > 0) { const { error } = await admin.from('profiles').update(patch).eq('id', id) if (error) return json({ error: error.message }, 400) } if (Object.keys(authPatch).length === 0 && Object.keys(patch).length === 0) { return json({ error: 'Rien à modifier.' }, 400) } return json({ ok: true }) } default: return json({ error: 'Action inconnue.' }, 400) } } catch (e) { return json({ error: (e as Error).message }, 500) } })