import { createServiceClient, getCorsHeaders, jsonResponse, preflightResponse, readJsonBody, } from "../_shared/security.ts"; const MAX_BODY_BYTES = 8 * 1024; const ADMIN_ROLES = new Set(["admin", "mega_admin"]); const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()); /** * Verify the caller's JWT and return their role from public.users. * Returns null if not authorized. */ async function getCallerRole(request: Request): Promise { const authHeader = request.headers.get("authorization") || ""; const token = authHeader.replace(/^Bearer\s+/i, "").trim(); if (!token) return null; const supabase = createServiceClient(); const { data: userData, error: userError } = await supabase.auth.getUser(token); if (userError || !userData?.user) return null; const userId = userData.user.id; const { data: userRow, error: roleError } = await supabase .from("users") .select("roles(name)") .eq("id", userId) .maybeSingle(); if (roleError || !userRow) return null; return userRow.roles?.name || null; } Deno.serve(async (request) => { if (request.method === "OPTIONS") { return preflightResponse(request, "private"); } const corsHeaders = getCorsHeaders(request, "private"); if (!corsHeaders) { return jsonResponse({ ok: false, error: "Origin not allowed" }, 403); } try { // ── Authorization ── const callerRole = await getCallerRole(request); if (!callerRole || !ADMIN_ROLES.has(callerRole)) { return jsonResponse( { ok: false, error: "Недостаточно прав. Требуется роль admin или mega_admin." }, 403, corsHeaders, ); } const supabase = createServiceClient(); // ── POST: create new user ── if (request.method === "POST") { const { body } = await readJsonBody<{ email?: string; name?: string; role?: string }>( request, { maxBytes: MAX_BODY_BYTES }, ); const email = String(body.email || "").trim().toLowerCase(); const name = String(body.name || "").trim(); const role = String(body.role || "").trim().toLowerCase(); if (!email || !isValidEmail(email)) { return jsonResponse({ ok: false, error: "Некорректный email" }, 400, corsHeaders); } if (!name) { return jsonResponse({ ok: false, error: "Имя обязательно" }, 400, corsHeaders); } if (!role) { return jsonResponse({ ok: false, error: "Роль обязательна" }, 400, corsHeaders); } // Check if email already exists in public.users const { data: existingUser } = await supabase .from("users") .select("id") .eq("email", email) .maybeSingle(); if (existingUser) { return jsonResponse({ ok: false, error: "Пользователь с таким email уже существует" }, 409, corsHeaders); } // Create auth user — trigger handle_new_user will auto-insert into public.users // using user_metadata.role and user_metadata.name const { data: authData, error: authError } = await supabase.auth.admin.createUser({ email, email_confirm: true, user_metadata: { name, role }, }); if (authError) { console.error("auth.createUser error:", authError); return jsonResponse( { ok: false, error: "Ошибка создания пользователя: " + authError.message }, 500, corsHeaders, ); } const newUserId = authData.user.id; return jsonResponse({ ok: true, data: { id: newUserId, email, name, role } }, 201, corsHeaders); } // ── DELETE: remove user ── if (request.method === "DELETE") { const url = new URL(request.url); const userId = url.searchParams.get("id"); if (!userId) { return jsonResponse({ ok: false, error: "Параметр id обязателен" }, 400, corsHeaders); } // Get user info before deletion const { data: userRow } = await supabase .from("users") .select("id, email, name") .eq("id", userId) .maybeSingle(); if (!userRow) { return jsonResponse({ ok: false, error: "Пользователь не найден" }, 404, corsHeaders); } // Delete auth user (FK ON DELETE CASCADE will remove public.users row) const { error: authDeleteError } = await supabase.auth.admin.deleteUser(userId); if (authDeleteError) { console.error("auth.deleteUser error:", authDeleteError); // Try deleting public.users directly as fallback const { error: dbDeleteError } = await supabase.from("users").delete().eq("id", userId); if (dbDeleteError) { return jsonResponse( { ok: false, error: "Ошибка удаления: " + authDeleteError.message }, 500, corsHeaders, ); } } return jsonResponse({ ok: true, data: { id: userId } }, 200, corsHeaders); } return jsonResponse({ ok: false, error: "Method not allowed" }, 405, corsHeaders); } catch (error) { if (error instanceof Error && "status" in error) { const httpError = error as { status: number; message: string }; return jsonResponse({ ok: false, error: httpError.message }, httpError.status, corsHeaders); } return jsonResponse( { ok: false, error: error instanceof Error ? error.message : "Unexpected error" }, 500, corsHeaders, ); } });