feat(admin): Phase V2-6 Admin CRM 고도화 — CRUD + 차트 + 결제 + 감사

- DB: audit_log 테이블(diff 포함) + subscriptions.admin_note + super_admin role
- Edge Functions 4개: admin-users, admin-subscriptions, admin-payments, admin-audit-log
- 공유 유틸: admin-auth.ts(권한 검증), audit.ts(감사로그 기록)
- Swagger UI: 독립 정적 페이지 + OpenAPI 3.0 spec
- CRUD 페이지: 구독 생성/수정/삭제, role 변경, 감사로그 목록/상세
- recharts: feature별 StackedBar + DAU Line + Top Users HorizontalBar
- 결제 이력: DB + Payple API 병행 조회
- 권한: super_admin만 위험 작업, admin은 조회 전용
- RLS: admin/super_admin IN 정책 + super_admin 쓰기 정책
- SQL RPC: admin_usage_by_feature, admin_top_users, admin_dau
This commit is contained in:
윤찬 2026-04-12 21:32:47 +09:00
parent f7c50eb2ed
commit dca1b90faa
34 changed files with 3142 additions and 45 deletions

View file

@ -0,0 +1,45 @@
// server/supabase/functions/_shared/admin-auth.ts
// Admin/Super-admin 권한 검증 — requireUser 확장
// @ts-expect-error — Deno 런타임 import
import type { User } from 'https://esm.sh/@supabase/supabase-js@2.39.7'
import { requireUser, type AuthError } from './auth.ts'
export type AdminRole = 'admin' | 'super_admin'
/**
* admin (admin, super_admin).
* AuthError throw.
*/
export async function requireAdmin(req: Request): Promise<User> {
const user = await requireUser(req)
const role = (user.app_metadata as Record<string, unknown>)?.role as string | undefined
if (role !== 'admin' && role !== 'super_admin') {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw { status: 403, message: 'Admin access required' } as AuthError
}
return user
}
/**
* super_admin .
* AuthError throw.
*/
export async function requireSuperAdmin(req: Request): Promise<User> {
const user = await requireUser(req)
const role = (user.app_metadata as Record<string, unknown>)?.role as string | undefined
if (role !== 'super_admin') {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw { status: 403, message: 'Super admin access required' } as AuthError
}
return user
}
/**
* admin role . admin이 null.
*/
export function getAdminRole(user: User): AdminRole | null {
const role = (user.app_metadata as Record<string, unknown>)?.role as string | undefined
if (role === 'admin' || role === 'super_admin') return role
return null
}