import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireAdmin, requireManager } from '../_shared/admin-auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' import { verifyCurrentAdminActor } from '../_shared/admin-current-role.ts' import { AdminPublicError, actorEmail, adminErrorResponse, adminJsonResponse, adminRpcError, managedRoleFilter, parsePagination, parseRoleChangeRequest, parseSearch, readAdminJson, requireIdempotencyKey, requireUuid, validateQueryKeys, validateRoleRpcResult, } from '../_shared/admin-contract.ts' async function readUsers(req: Request): Promise { const admin = await requireManager(req) const url = new URL(req.url) validateQueryKeys(url, ['userId', 'page', 'limit', 'search', 'role']) const serviceClient = createServiceRoleClient() await verifyCurrentAdminActor(serviceClient, admin) const userIdValue = url.searchParams.get('userId') if (userIdValue) { const userId = requireUuid(userIdValue, 'invalid_userId') const [{ data: profile, error: profileError }, { data: subscription, error: subscriptionError }, accountResult] = await Promise.all([ serviceClient .from('profiles') .select('id, name, avatar_url, locale, tier, role, created_at, updated_at') .eq('id', userId) .maybeSingle(), serviceClient .from('subscriptions') .select('id, user_id, tier, status, provider, payment_provider, current_period_start, current_period_end, overage_credits, admin_note, cancel_at, created_at, updated_at') .eq('user_id', userId) .maybeSingle(), serviceClient.auth.admin.getUserById(userId), ]) if (profileError || subscriptionError || accountResult.error) { throw new AdminPublicError(500, 'admin_user_read_failed') } if (!profile || !accountResult.data.user) throw new AdminPublicError(404, 'user_not_found') const account = accountResult.data.user return adminJsonResponse({ profile, subscription: subscription ?? null, account: { id: account.id, email: account.email ?? null, createdAt: account.created_at, lastSignInAt: account.last_sign_in_at ?? null, }, }, corsHeaders) } const { page, limit, from, to } = parsePagination(url) const search = parseSearch(url.searchParams.get('search')) const role = managedRoleFilter(url.searchParams.get('role')) let query = serviceClient .from('profiles') .select('id, name, avatar_url, tier, role, created_at', { count: 'exact' }) if (search) { const literalPattern = search.replace(/[\\%_]/g, '\\$&') query = query.ilike('name', `%${literalPattern}%`) } if (role) query = query.eq('role', role) const { data, count, error } = await query .order('created_at', { ascending: false }) .range(from, to) if (error) throw new AdminPublicError(500, 'admin_user_read_failed') return adminJsonResponse({ profiles: data ?? [], total: count ?? 0, page, limit }, corsHeaders) } async function changeRole(req: Request): Promise { const admin = await requireAdmin(req) const body = await readAdminJson(req) const mutation = parseRoleChangeRequest(body, requireIdempotencyKey(req)) const serviceClient = createServiceRoleClient() const { data, error } = await serviceClient.rpc('admin_change_user_role_v1', { p_actor_email: actorEmail(admin), p_idempotency_key: mutation.idempotencyKey, p_target_user_id: mutation.userId, p_new_role: mutation.newRole, p_memo: mutation.memo, }) if (error) throw adminRpcError(error) return adminJsonResponse(validateRoleRpcResult(data, mutation), corsHeaders) } export async function handleAdminUsers(req: Request): Promise { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight try { if (req.method === 'GET') return await readUsers(req) if (req.method === 'PATCH') return await changeRole(req) return adminJsonResponse({ error: 'method_not_allowed' }, corsHeaders, 405) } catch (error) { console.error('admin-users request failed', error) return adminErrorResponse(error, corsHeaders) } } if (import.meta.main) Deno.serve(handleAdminUsers)