d3ro-voice/server/supabase/functions/admin-audit-log/index.ts
2026-08-29 18:33:45 +09:00

117 lines
4.7 KiB
TypeScript

import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts'
import { requireManager } from '../_shared/admin-auth.ts'
import { createServiceRoleClient } from '../_shared/quota.ts'
import { verifyCurrentAdminActor } from '../_shared/admin-current-role.ts'
import {
AdminPublicError,
adminErrorResponse,
adminJsonResponse,
parseIsoDate,
parsePagination,
parsePositiveInteger,
requireUuid,
sanitizeAuditRecord,
validateQueryKeys,
} from '../_shared/admin-contract.ts'
const AUDIT_COLUMNS = 'id, admin_id, action, target_type, target_id, before_data, after_data, memo, created_at'
function targetTypeFilter(value: string | null): string | undefined {
if (value === null || value === '') return undefined
if (value !== 'subscription' && value !== 'profile') {
throw new AdminPublicError(400, 'invalid_target_type')
}
return value
}
async function readAuditLog(req: Request): Promise<Response> {
const admin = await requireManager(req)
const url = new URL(req.url)
validateQueryKeys(url, ['id', 'page', 'limit', 'target_type', 'admin_id', 'target_id', 'from', 'to'])
const serviceClient = createServiceRoleClient()
await verifyCurrentAdminActor(serviceClient, admin)
const logId = parsePositiveInteger(url.searchParams.get('id'), 'invalid_audit_id')
if (logId !== undefined) {
const { data: log, error: logError } = await serviceClient
.from('audit_log')
.select(AUDIT_COLUMNS)
.eq('id', logId)
.maybeSingle()
if (logError) throw new AdminPublicError(500, 'admin_audit_read_failed')
if (!log) throw new AdminPublicError(404, 'audit_log_not_found')
if (typeof log.admin_id !== 'string') throw new AdminPublicError(502, 'invalid_admin_read_response')
const { data: admin, error: adminError } = await serviceClient
.from('profiles')
.select('id, name, avatar_url')
.eq('id', log.admin_id)
.maybeSingle()
if (adminError) throw new AdminPublicError(500, 'admin_audit_read_failed')
return adminJsonResponse({ log: sanitizeAuditRecord(log), admin: admin ?? null }, corsHeaders)
}
const { page, limit, from, to } = parsePagination(url)
const targetType = targetTypeFilter(url.searchParams.get('target_type'))
const adminIdValue = url.searchParams.get('admin_id')
const targetIdValue = url.searchParams.get('target_id')
const adminId = adminIdValue ? requireUuid(adminIdValue, 'invalid_admin_id') : undefined
const targetId = targetIdValue ? requireUuid(targetIdValue, 'invalid_target_id') : undefined
const fromDate = parseIsoDate(url.searchParams.get('from'), 'invalid_from_date')
const toDate = parseIsoDate(url.searchParams.get('to'), 'invalid_to_date')
if (fromDate && toDate && fromDate > toDate) throw new AdminPublicError(400, 'invalid_date_range')
let query = serviceClient.from('audit_log').select(AUDIT_COLUMNS, { 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:00.000Z`)
if (toDate) query = query.lte('created_at', `${toDate}T23:59:59.999Z`)
const { data, count, error } = await query
.order('created_at', { ascending: false })
.range(from, to)
if (error) throw new AdminPublicError(500, 'admin_audit_read_failed')
const logs = (data ?? []) as Array<Record<string, unknown>>
const adminIds = [...new Set(logs
.map((log) => log.admin_id)
.filter((value): value is string => typeof value === 'string'))]
const adminNames = new Map<string, string | null>()
if (adminIds.length > 0) {
const { data: admins, error: adminsError } = await serviceClient
.from('profiles')
.select('id, name')
.in('id', adminIds)
if (adminsError) throw new AdminPublicError(500, 'admin_audit_read_failed')
for (const admin of (admins ?? []) as Array<{ id: string; name: string | null }>) {
adminNames.set(admin.id, admin.name)
}
}
return adminJsonResponse({
logs: logs.map((log) => ({
...sanitizeAuditRecord(log),
admin_name: typeof log.admin_id === 'string' ? adminNames.get(log.admin_id) ?? null : null,
})),
total: count ?? 0,
page,
limit,
}, corsHeaders)
}
export async function handleAdminAuditLog(req: Request): Promise<Response> {
const preflight = handleCorsPreflightRequest(req)
if (preflight) return preflight
try {
if (req.method !== 'GET') {
return adminJsonResponse({ error: 'method_not_allowed' }, corsHeaders, 405)
}
return await readAuditLog(req)
} catch (error) {
console.error('admin-audit-log request failed', error)
return adminErrorResponse(error, corsHeaders)
}
}
if (import.meta.main) Deno.serve(handleAdminAuditLog)