This commit is contained in:
2026-08-10 22:06:49 +02:00
parent 2a5d9bb0fd
commit 8bff7982ff
83 changed files with 17975 additions and 4824 deletions
+76 -57
View File
@@ -1,12 +1,14 @@
// =====================================================================
// Edge Function : admin-users
// Gère les comptes (lister / créer / supprimer / modifier rôle & admin).
// 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'appli.
// sécurité — elle n'est JAMAIS exposée dans l'application.
//
// Déploiement (voir le guide) :
// Déploiement :
// supabase functions deploy admin-users --no-verify-jwt
// (la fonction vérifie elle-même l'identité de l'appelant ci-dessous.)
// (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'
@@ -20,6 +22,17 @@ 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,
@@ -27,8 +40,13 @@ function json(body: unknown, status = 200): Response {
})
}
/** 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) => {
// Pré-vol CORS (envoyé automatiquement par le navigateur/webview).
if (req.method === 'OPTIONS') return new Response('ok', { headers: cors })
try {
@@ -43,7 +61,7 @@ Deno.serve(async (req) => {
} = await caller.auth.getUser()
if (userErr || !user) return json({ error: 'Non authentifié.' }, 401)
// --- 2) Client "admin" (service_role) pour les opérations sensibles ---
// --- 2) Client « admin » (service_role) pour les opérations sensibles ---
const admin = createClient(SUPABASE_URL, SERVICE_ROLE, {
auth: { autoRefreshToken: false, persistSession: false }
})
@@ -61,63 +79,61 @@ Deno.serve(async (req) => {
switch (action) {
case 'list': {
// Emails dans auth.users + métadonnées dans profiles → on fusionne.
// 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: any) => [pr.id, pr]))
const users = list.users.map((u) => {
const pr: any = byId.get(u.id) ?? {}
const roles =
Array.isArray(pr.roles) && pr.roles.length
? pr.roles
: pr.role
? [pr.role]
: []
return {
id: u.id,
email: u.email ?? '',
full_name: pr.full_name ?? '',
role: pr.role ?? 'other',
roles,
is_admin: pr.is_admin ?? false,
avatar_color: pr.avatar_color ?? '#7c5cff',
avatar_url: pr.avatar_url ?? null,
created_at: pr.created_at ?? u.created_at
}
})
const byId = new Map((profiles ?? []).map((pr: Record<string, unknown>) => [pr.id, pr]))
const users = list.users
.map((u) => {
const pr = (byId.get(u.id) ?? {}) as Record<string, unknown>
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, role, roles, is_admin } = p
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)
if (String(password).length < 6) {
return json({ error: 'Le mot de passe doit faire au moins 6 caractères.' }, 400)
}
// Liste de pôles (rôles) ; le premier fait office de rôle principal.
const roleList: string[] = Array.isArray(roles) && roles.length ? roles : role ? [role] : []
const roleList = cleanRoles(roles)
const primary = roleList[0] ?? 'other'
const name = full_name || String(email).split('@')[0]
// Crée le compte (email déjà confirmé : connexion immédiate possible).
// 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: full_name || email.split('@')[0], role: primary }
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.
await admin
const { error: profErr } = await admin
.from('profiles')
.update({
full_name: full_name || email.split('@')[0],
role: primary,
full_name: name,
roles: roleList.length ? roleList : [primary],
is_admin: !!is_admin
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 })
}
@@ -125,58 +141,61 @@ Deno.serve(async (req) => {
case 'delete': {
const { id } = p
if (!id) return json({ error: 'Identifiant manquant.' }, 400)
if (id === user.id)
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, role, roles, is_admin, avatar_url } = p
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)
if (id === user.id && is_admin === false) {
return json({ error: 'Tu ne peux pas retirer ton propre statut admin.' }, 400)
}
// --- a) Champs du compte auth (email / mot de passe / métadonnées) ---
// --- a) Compte auth (email / mot de passe / métadonnées) ---
const authPatch: Record<string, unknown> = {}
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)
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)
authPatch.user_metadata = { full_name, role: Array.isArray(roles) ? roles[0] : role }
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) Champs du profil (nom, pôles, admin) ---
// --- b) Profil (nom, pôles, admin, avatar) ---
const patch: Record<string, unknown> = {}
if (full_name !== undefined) patch.full_name = full_name
if (is_admin !== undefined) patch.is_admin = !!is_admin
if (is_admin !== undefined) patch.is_admin = Boolean(is_admin)
if (avatar_url !== undefined) patch.avatar_url = avatar_url || null
// Gestion des pôles : roles = liste complète, role = principal (roles[0]).
if (Array.isArray(roles)) {
const primary = roles[0] ?? 'other'
patch.roles = roles.length ? roles : [primary]
patch.role = primary
} else if (role !== undefined) {
patch.role = role
patch.roles = [role]
}
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)
if (Object.keys(authPatch).length === 0 && Object.keys(patch).length === 0) {
return json({ error: 'Rien à modifier.' }, 400)
}
return json({ ok: true })
}