Panel admin, planning, tags, suivi du temps, thèmes clair/sombre
- Admin: création/suppression de comptes + rôles via Edge Function sécurisée - Config Supabase intégrée au build (.env), écran Setup supprimé - Page Planning (vue mois/semaine) - Tags "jobs" sur les tâches + filtres (membre / tag) - Suivi du temps par tâche, total par jalon - Toasts + confirmations in-app (fini alert/confirm natifs) - Thème Notion clair + mode sombre (accent monochrome, bascule persistée) - .env suivi par git (public uniquement) pour synchro multi-machines Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
// =====================================================================
|
||||
// Edge Function : admin-users
|
||||
// Gère les comptes (lister / créer / supprimer / modifier rôle & admin).
|
||||
// Tourne sur les serveurs Supabase et garde la clé service_role en
|
||||
// sécurité — elle n'est JAMAIS exposée dans l'appli.
|
||||
//
|
||||
// Déploiement (voir le guide) :
|
||||
// supabase functions deploy admin-users --no-verify-jwt
|
||||
// (la fonction vérifie elle-même l'identité de l'appelant ci-dessous.)
|
||||
// =====================================================================
|
||||
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')!
|
||||
|
||||
function json(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { ...cors, 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// Pré-vol CORS (envoyé automatiquement par le navigateur/webview).
|
||||
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': {
|
||||
// Emails dans auth.users + métadonnées dans profiles → on fusionne.
|
||||
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) ?? {}
|
||||
return {
|
||||
id: u.id,
|
||||
email: u.email ?? '',
|
||||
full_name: pr.full_name ?? '',
|
||||
role: pr.role ?? 'other',
|
||||
is_admin: pr.is_admin ?? false,
|
||||
created_at: pr.created_at ?? u.created_at
|
||||
}
|
||||
})
|
||||
return json(users)
|
||||
}
|
||||
|
||||
case 'create': {
|
||||
const { email, password, full_name, role, 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)
|
||||
|
||||
// Crée le compte (email déjà confirmé : connexion immédiate possible).
|
||||
const { data: created, error } = await admin.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: true,
|
||||
user_metadata: { full_name: full_name || email.split('@')[0], role: role || 'other' }
|
||||
})
|
||||
if (error) return json({ error: error.message }, 400)
|
||||
|
||||
// Le trigger handle_new_user a créé le profil ; on cale les champs.
|
||||
await admin
|
||||
.from('profiles')
|
||||
.update({
|
||||
full_name: full_name || email.split('@')[0],
|
||||
role: role || 'other',
|
||||
is_admin: !!is_admin
|
||||
})
|
||||
.eq('id', created.user.id)
|
||||
|
||||
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, role, is_admin } = 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)
|
||||
|
||||
const patch: Record<string, unknown> = {}
|
||||
if (full_name !== undefined) patch.full_name = full_name
|
||||
if (role !== undefined) patch.role = role
|
||||
if (is_admin !== undefined) patch.is_admin = !!is_admin
|
||||
if (Object.keys(patch).length === 0) return json({ error: 'Rien à modifier.' }, 400)
|
||||
|
||||
const { error } = await admin.from('profiles').update(patch).eq('id', id)
|
||||
if (error) return json({ error: error.message }, 400)
|
||||
return json({ ok: true })
|
||||
}
|
||||
|
||||
default:
|
||||
return json({ error: 'Action inconnue.' }, 400)
|
||||
}
|
||||
} catch (e) {
|
||||
return json({ error: (e as Error).message }, 500)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user