import { serve } from "https://deno.land/std@0.203.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; // CORS: allow browser-based web clients (adjust origin in production) // Permit headers the supabase-js client sends (e.g. `apikey`, `x-client-info`) so // browser preflight requests succeed. Keep origin wide for dev; in production // prefer echoing the request origin and enabling credentials only when needed. const CORS_HEADERS: Record = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, x-client-info, x-requested-with, Origin, Accept", "Access-Control-Max-Age": "3600", }; const jsonResponse = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...CORS_HEADERS }, }); serve(async (req) => { // Handle CORS preflight quickly so browsers can call this function from localhost/dev if (req.method === "OPTIONS") { return new Response(null, { status: 204, headers: CORS_HEADERS }); } if (req.method != "POST") { return jsonResponse({ error: "Method not allowed" }, 405); } const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? ""; const anonKey = Deno.env.get("SUPABASE_ANON_KEY") ?? ""; const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; if (!supabaseUrl || !anonKey || !serviceKey) { return jsonResponse({ error: "Missing env configuration" }, 500); } const authHeader = req.headers.get("Authorization") ?? ""; const token = authHeader.replace("Bearer ", "").trim(); if (!token) { return jsonResponse({ error: "Missing access token" }, 401); } const authClient = createClient(supabaseUrl, anonKey, { global: { headers: { Authorization: `Bearer ${token}` } }, }); const { data: authData, error: authError } = await authClient.auth.getUser(); if (authError || !authData?.user) { return jsonResponse({ error: "Unauthorized" }, 401); } const adminClient = createClient(supabaseUrl, serviceKey); const { data: profile, error: profileError } = await adminClient .from("profiles") .select("role") .eq("id", authData.user.id) .maybeSingle(); const role = (profile?.role ?? "").toString().toLowerCase(); if (profileError || role != "admin") { return jsonResponse({ error: "Forbidden" }, 403); } let payload: Record = {}; try { payload = await req.json(); } catch (_) { return jsonResponse({ error: "Invalid JSON" }, 400); } const action = payload.action as string | undefined; const userId = payload.userId as string | undefined; if (!action) { return jsonResponse({ error: "Missing action" }, 400); } // `list_users` does not require a target userId; other actions do. if (action !== "list_users" && !userId) { return jsonResponse({ error: "Missing userId" }, 400); } if (action === "list_users") { const offset = Number(payload.offset ?? 0); const limit = Number(payload.limit ?? 50); const searchQuery = (payload.searchQuery ?? "").toString().trim(); // Fetch paginated profiles first (enforce server-side pagination) let profilesQuery = adminClient .from("profiles") .select("id, full_name, role") .order("id", { ascending: true }) .range(offset, offset + limit - 1); if (searchQuery.length > 0) { profilesQuery = adminClient .from("profiles") .select("id, full_name, role") .ilike("full_name", `%${searchQuery}%`) .order("id", { ascending: true }) .range(offset, offset + limit - 1); } const { data: profiles, error: profilesError } = await profilesQuery; if (profilesError) return jsonResponse({ error: profilesError.message }, 500); const users = []; for (const p of (profiles ?? [])) { const { data: userResp } = await adminClient.auth.admin.getUserById(p.id); users.push({ id: p.id, full_name: p.full_name ?? null, role: p.role ?? null, email: userResp?.user?.email ?? null, bannedUntil: userResp?.user?.banned_until ?? null, }); } return jsonResponse({ users }); } if (action == "get_user") { const { data, error } = await adminClient.auth.admin.getUserById(userId); if (error) { return jsonResponse({ error: error.message }, 400); } return jsonResponse({ user: data.user }); } if (action == "set_password") { const password = payload.password as string | undefined; if (!password || password.length < 8) { return jsonResponse({ error: "Password must be at least 8 characters" }, 400); } const { error } = await adminClient.auth.admin.updateUserById(userId, { password, }); if (error) { return jsonResponse({ error: error.message }, 400); } return jsonResponse({ ok: true }); } if (action == "set_lock") { const locked = Boolean(payload.locked); const { error } = await adminClient.auth.admin.updateUserById(userId, { ban_duration: locked ? "100y" : "0s", }); if (error) { return jsonResponse({ error: error.message }, 400); } return jsonResponse({ ok: true }); } return jsonResponse({ error: "Unknown action" }, 400); });