// server/supabase/functions/admin-users/index.ts // Admin: 유저 목록/상세 조회 + super_admin: role 변경 import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { authErrorResponse, type AuthError } from '../_shared/auth.ts' import { requireAdmin, requireSuperAdmin } from '../_shared/admin-auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' import { writeAuditLog } from '../_shared/audit.ts' // @ts-expect-error — Deno 런타임 import import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.7' function jsonResponse(body: Record | Record[], status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } interface RoleChangeBody { userId: string newRole: 'user' | 'admin' | 'super_admin' memo: string } // @ts-expect-error — Deno 런타임 전역 Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight try { const url = new URL(req.url) // ── GET: 유저 목록/상세 ── if (req.method === 'GET') { const admin = await requireAdmin(req) const serviceClient = createServiceRoleClient() const userId = url.searchParams.get('userId') if (userId) { // 유저 상세 const { data: profile, error } = await serviceClient .from('profiles') .select('id, name, avatar_url, locale, tier, role, created_at, updated_at') .eq('id', userId) .maybeSingle() if (error) return jsonResponse({ error: error.message }, 500) if (!profile) return jsonResponse({ error: 'User not found' }, 404) const { data: sub } = await serviceClient .from('subscriptions') .select('*') .eq('user_id', userId) .maybeSingle() return jsonResponse({ profile, subscription: sub }) } // 유저 목록 const page = parseInt(url.searchParams.get('page') ?? '1', 10) const limit = parseInt(url.searchParams.get('limit') ?? '20', 10) const search = url.searchParams.get('search') ?? '' const roleFilter = url.searchParams.get('role') ?? '' const from = (page - 1) * limit const to = from + limit - 1 let query = serviceClient .from('profiles') .select('id, name, avatar_url, tier, role, created_at', { count: 'exact' }) if (search) { query = query.ilike('name', `%${search}%`) } if (roleFilter) { query = query.eq('role', roleFilter) } const { data: profiles, count, error } = await query .order('created_at', { ascending: false }) .range(from, to) if (error) return jsonResponse({ error: error.message }, 500) return jsonResponse({ profiles: profiles ?? [], total: count ?? 0, page, limit }) } // ── PATCH: role 변경 (super_admin 전용) ── if (req.method === 'PATCH') { const admin = await requireSuperAdmin(req) const body = (await req.json()) as RoleChangeBody const serviceClient = createServiceRoleClient() if (!body.userId || !body.newRole || !body.memo) { return jsonResponse({ error: 'userId, newRole, memo are required' }, 400) } const validRoles = ['user', 'admin', 'super_admin'] if (!validRoles.includes(body.newRole)) { return jsonResponse({ error: `Invalid role. Must be one of: ${validRoles.join(', ')}` }, 400) } // 현재 프로필 조회 (before 스냅샷) const { data: before } = await serviceClient .from('profiles') .select('id, name, role') .eq('id', body.userId) .maybeSingle() if (!before) return jsonResponse({ error: 'User not found' }, 404) // 1. auth.users.raw_app_meta_data.role 변경 const { error: authError } = await serviceClient.auth.admin.updateUserById(body.userId, { app_metadata: { role: body.newRole }, }) if (authError) return jsonResponse({ error: `Auth update failed: ${authError.message}` }, 500) // 2. profiles.role 동기화 const { error: profileError } = await serviceClient .from('profiles') .update({ role: body.newRole, updated_at: new Date().toISOString() }) .eq('id', body.userId) if (profileError) return jsonResponse({ error: `Profile update failed: ${profileError.message}` }, 500) // 3. 감사로그 await writeAuditLog(serviceClient, { adminId: admin.id, action: 'user.role_change', targetType: 'profile', targetId: body.userId, beforeData: { role: before.role }, afterData: { role: body.newRole }, memo: body.memo, }) return jsonResponse({ success: true, userId: body.userId, newRole: body.newRole }) } return jsonResponse({ error: 'Method not allowed' }, 405) } catch (err) { if (err && typeof err === 'object' && 'status' in err && 'message' in err) { return authErrorResponse(err as AuthError, corsHeaders) } const message = err instanceof Error ? err.message : 'Unknown error' return jsonResponse({ error: message }, 500) } })