d3ro-voice/server/supabase/functions/admin-audit-log/index.ts
윤찬 8af75a0a1e feat: V2-7 Admin 콘솔 리디자인 + 3단계 권한 + SaaS 전환 + 코드 정리
- Admin CRM: D3RO Console 스타일 전체 적용 (panelSx/tableSx/filterBtnSx)
- 3단계 권한: manager/admin/super_admin (DB + Edge Functions + Frontend)
- 랜딩 페이지: 1회 결제 → 월간/연간 구독 SaaS 모델 (10개 언어)
- SSE 스트리밍: VoiceConversation Premium LLM 라우팅 + fallback
- Supabase 클라이언트: packages/api-client 공통 추출 (browser+server)
- RPC 함수 타입: 9개 정의 (admin_usage_by_feature 등)
- callAdminApi 401 버그 수정 (getUser() 선행 토큰 갱신)
2026-04-13 01:28:10 +09:00

113 lines
3.9 KiB
TypeScript

// server/supabase/functions/admin-audit-log/index.ts
// 감사로그 조회 — manager 이상
import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts'
import { authErrorResponse, type AuthError } from '../_shared/auth.ts'
import { requireManager } from '../_shared/admin-auth.ts'
import { createServiceRoleClient } from '../_shared/quota.ts'
function jsonResponse(body: Record<string, unknown> | Record<string, unknown>[], status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
// @ts-expect-error — Deno 런타임 전역
Deno.serve(async (req: Request) => {
const preflight = handleCorsPreflightRequest(req)
if (preflight) return preflight
if (req.method !== 'GET') {
return jsonResponse({ error: 'Method not allowed' }, 405)
}
try {
await requireManager(req)
const url = new URL(req.url)
const serviceClient = createServiceRoleClient()
// 단건 상세
const logId = url.searchParams.get('id')
if (logId) {
const { data: log, error } = await serviceClient
.from('audit_log')
.select('*')
.eq('id', parseInt(logId, 10))
.maybeSingle()
if (error) return jsonResponse({ error: error.message }, 500)
if (!log) return jsonResponse({ error: 'Audit log not found' }, 404)
// admin 프로필 정보 함께
const { data: adminProfile } = await serviceClient
.from('profiles')
.select('id, name, avatar_url')
.eq('id', (log as Record<string, unknown>).admin_id)
.maybeSingle()
return jsonResponse({
log: log as unknown as Record<string, unknown>,
admin: adminProfile as unknown as Record<string, unknown>,
})
}
// 목록 조회
const page = parseInt(url.searchParams.get('page') ?? '1', 10)
const limit = parseInt(url.searchParams.get('limit') ?? '20', 10)
const targetType = url.searchParams.get('target_type') ?? ''
const adminId = url.searchParams.get('admin_id') ?? ''
const fromDate = url.searchParams.get('from') ?? ''
const toDate = url.searchParams.get('to') ?? ''
const targetId = url.searchParams.get('target_id') ?? ''
const rangeFrom = (page - 1) * limit
const rangeTo = rangeFrom + limit - 1
let query = serviceClient
.from('audit_log')
.select('*', { count: 'exact' })
if (targetType) query = query.eq('target_type', targetType)
if (adminId) query = query.eq('admin_id', adminId)
if (targetId) query = query.eq('target_id', targetId)
if (fromDate) query = query.gte('created_at', `${fromDate}T00:00:00Z`)
if (toDate) query = query.lte('created_at', `${toDate}T23:59:59Z`)
const { data, count, error } = await query
.order('created_at', { ascending: false })
.range(rangeFrom, rangeTo)
if (error) return jsonResponse({ error: error.message }, 500)
// admin 이름 매핑
const logs = (data ?? []) as unknown as Record<string, unknown>[]
const adminIds = [...new Set(logs.map(l => l.admin_id as string))]
let adminMap: Record<string, string> = {}
if (adminIds.length > 0) {
const { data: admins } = await serviceClient
.from('profiles')
.select('id, name')
.in('id', adminIds)
if (admins) {
adminMap = Object.fromEntries(
(admins as Array<{ id: string; name: string | null }>).map(a => [a.id, a.name ?? 'Unknown'])
)
}
}
const enrichedLogs = logs.map(log => ({
...log,
admin_name: adminMap[log.admin_id as string] ?? 'Unknown',
}))
return jsonResponse({ logs: enrichedLogs, total: count ?? 0, page, limit })
} 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)
}
})