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,100 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>D3RO-VOICE Admin API</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" />
<style>
body { margin: 0; padding: 0; background: #1a1a2e; }
.topbar { display: none !important; }
.swagger-ui .info .title { color: #e0e0e0; }
.swagger-ui { max-width: 1200px; margin: 0 auto; }
#header {
background: linear-gradient(135deg, #0f0f23, #1a1a2e);
color: #e0e0e0;
padding: 20px 32px;
border-bottom: 1px solid #333;
font-family: system-ui, -apple-system, sans-serif;
}
#header h1 { margin: 0 0 4px; font-size: 1.4rem; }
#header p { margin: 0; font-size: 0.85rem; color: #888; }
#auth-bar {
background: #0f0f23;
padding: 12px 32px;
display: flex;
gap: 8px;
align-items: center;
border-bottom: 1px solid #222;
font-family: monospace;
}
#auth-bar label { color: #aaa; font-size: 0.8rem; }
#auth-bar input {
flex: 1;
max-width: 500px;
padding: 6px 10px;
background: #1a1a2e;
border: 1px solid #444;
border-radius: 4px;
color: #e0e0e0;
font-family: monospace;
font-size: 0.8rem;
}
#auth-bar button {
padding: 6px 16px;
background: #4a6cf7;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
#auth-bar button:hover { background: #3a5ce5; }
</style>
</head>
<body>
<div id="header">
<h1>D3RO-VOICE Admin API</h1>
<p>Admin CRM Edge Functions — User management, Subscription CRUD, Payments, Audit logs</p>
</div>
<div id="auth-bar">
<label for="token">Bearer Token:</label>
<input id="token" type="text" placeholder="Paste your Supabase access_token here..." />
<button onclick="applyToken()">Apply</button>
</div>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
let swaggerUI;
window.onload = function () {
swaggerUI = SwaggerUIBundle({
url: './openapi.json',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis],
layout: 'BaseLayout',
requestInterceptor: function (req) {
const token = document.getElementById('token').value.trim();
if (token) {
req.headers['Authorization'] = 'Bearer ' + token;
}
return req;
},
});
};
function applyToken() {
const token = document.getElementById('token').value.trim();
if (token) {
swaggerUI.preauthorizeApiKey('BearerAuth', token);
document.getElementById('token').style.borderColor = '#4a6cf7';
setTimeout(function () {
document.getElementById('token').style.borderColor = '#444';
}, 1000);
}
}
</script>
</body>
</html>

View file

@ -0,0 +1,336 @@
{
"openapi": "3.0.3",
"info": {
"title": "D3RO-VOICE Admin API",
"description": "Admin CRM Edge Functions for user management, subscription CRUD, payment history, and audit logs.",
"version": "1.0.0"
},
"servers": [
{
"url": "https://{supabaseRef}.supabase.co/functions/v1",
"description": "Production",
"variables": {
"supabaseRef": {
"default": "your-project-ref"
}
}
},
{
"url": "http://localhost:54321/functions/v1",
"description": "Local development"
}
],
"security": [
{
"BearerAuth": []
}
],
"components": {
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "Supabase access_token (admin or super_admin role required)"
}
},
"schemas": {
"Profile": {
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"name": { "type": "string", "nullable": true },
"avatar_url": { "type": "string", "nullable": true },
"locale": { "type": "string" },
"tier": { "type": "string", "enum": ["free", "pro", "pro_plus"] },
"role": { "type": "string", "enum": ["user", "admin", "super_admin"] },
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" }
}
},
"Subscription": {
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"user_id": { "type": "string", "format": "uuid" },
"tier": { "type": "string", "enum": ["free", "pro", "pro_plus"] },
"status": { "type": "string", "enum": ["active", "canceled", "past_due", "expired"] },
"payment_provider": { "type": "string", "enum": ["none", "stripe", "payple"] },
"current_period_start": { "type": "string", "format": "date-time", "nullable": true },
"current_period_end": { "type": "string", "format": "date-time", "nullable": true },
"overage_credits": { "type": "integer" },
"admin_note": { "type": "string", "nullable": true },
"renewal_failures": { "type": "integer" },
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" }
}
},
"AuditLog": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"admin_id": { "type": "string", "format": "uuid" },
"admin_name": { "type": "string" },
"action": { "type": "string" },
"target_type": { "type": "string" },
"target_id": { "type": "string", "format": "uuid" },
"before_data": { "type": "object", "nullable": true },
"after_data": { "type": "object", "nullable": true },
"memo": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" }
}
},
"Error": {
"type": "object",
"properties": {
"error": { "type": "string" }
}
}
}
},
"paths": {
"/admin-users": {
"get": {
"tags": ["Users"],
"summary": "List or get user details",
"description": "Admin+. Pass userId for single user detail, or omit for paginated list.",
"parameters": [
{ "name": "userId", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Specific user ID for detail view" },
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
{ "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20 } },
{ "name": "search", "in": "query", "schema": { "type": "string" }, "description": "Name search (ilike)" },
{ "name": "role", "in": "query", "schema": { "type": "string", "enum": ["user", "admin", "super_admin"] } }
],
"responses": {
"200": {
"description": "User list or detail",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"type": "object",
"properties": {
"profiles": { "type": "array", "items": { "$ref": "#/components/schemas/Profile" } },
"total": { "type": "integer" },
"page": { "type": "integer" },
"limit": { "type": "integer" }
}
},
{
"type": "object",
"properties": {
"profile": { "$ref": "#/components/schemas/Profile" },
"subscription": { "$ref": "#/components/schemas/Subscription" }
}
}
]
}
}
}
},
"403": { "description": "Not admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
}
},
"patch": {
"tags": ["Users"],
"summary": "Change user role",
"description": "Super admin only. Changes both auth.users.app_metadata.role and profiles.role.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["userId", "newRole", "memo"],
"properties": {
"userId": { "type": "string", "format": "uuid" },
"newRole": { "type": "string", "enum": ["user", "admin", "super_admin"] },
"memo": { "type": "string", "description": "Required reason for audit log" }
}
}
}
}
},
"responses": {
"200": { "description": "Role changed successfully" },
"403": { "description": "Not super_admin" }
}
}
},
"/admin-subscriptions": {
"get": {
"tags": ["Subscriptions"],
"summary": "List or get subscription details",
"parameters": [
{ "name": "userId", "in": "query", "schema": { "type": "string", "format": "uuid" } },
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
{ "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20 } },
{ "name": "status", "in": "query", "schema": { "type": "string", "enum": ["active", "canceled", "past_due", "expired"] } },
{ "name": "tier", "in": "query", "schema": { "type": "string", "enum": ["free", "pro", "pro_plus"] } }
],
"responses": {
"200": { "description": "Subscription list or detail" }
}
},
"post": {
"tags": ["Subscriptions"],
"summary": "Create subscription (VIP grant / record recovery)",
"description": "Super admin only.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["userId", "tier", "memo"],
"properties": {
"userId": { "type": "string", "format": "uuid" },
"tier": { "type": "string", "enum": ["free", "pro", "pro_plus"] },
"status": { "type": "string", "enum": ["active", "canceled", "past_due", "expired"], "default": "active" },
"currentPeriodEnd": { "type": "string", "format": "date-time" },
"adminNote": { "type": "string" },
"memo": { "type": "string" }
}
}
}
}
},
"responses": {
"201": { "description": "Subscription created" },
"409": { "description": "Subscription already exists" }
}
},
"patch": {
"tags": ["Subscriptions"],
"summary": "Update subscription",
"description": "Super admin only.",
"parameters": [
{ "name": "userId", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" } }
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["memo"],
"properties": {
"tier": { "type": "string", "enum": ["free", "pro", "pro_plus"] },
"status": { "type": "string", "enum": ["active", "canceled", "past_due", "expired"] },
"currentPeriodEnd": { "type": "string", "format": "date-time" },
"overageCredits": { "type": "integer" },
"adminNote": { "type": "string" },
"memo": { "type": "string" }
}
}
}
}
},
"responses": {
"200": { "description": "Subscription updated" }
}
},
"delete": {
"tags": ["Subscriptions"],
"summary": "Soft-delete subscription",
"description": "Super admin only. Sets status=expired, tier=free.",
"parameters": [
{ "name": "userId", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" } }
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["memo"],
"properties": {
"memo": { "type": "string" }
}
}
}
}
},
"responses": {
"200": { "description": "Subscription soft-deleted" }
}
}
},
"/admin-payments": {
"get": {
"tags": ["Payments"],
"summary": "Get payment history for a user",
"description": "Admin+. Returns DB subscription data + audit logs. Pass source=payple for Payple API history.",
"parameters": [
{ "name": "userId", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" } },
{ "name": "source", "in": "query", "schema": { "type": "string", "enum": ["db", "payple"] }, "description": "Add 'payple' to also fetch from Payple API" }
],
"responses": {
"200": {
"description": "Payment history",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"subscription": { "$ref": "#/components/schemas/Subscription" },
"auditLogs": { "type": "array", "items": { "$ref": "#/components/schemas/AuditLog" } },
"paypleHistory": { "type": "object", "description": "Payple API response (when source=payple)" },
"paypleError": { "type": "string", "description": "Error message if Payple API call failed" }
}
}
}
}
}
}
}
},
"/admin-audit-log": {
"get": {
"tags": ["Audit Log"],
"summary": "List or get audit log entries",
"description": "Admin+. Pass id for single entry detail.",
"parameters": [
{ "name": "id", "in": "query", "schema": { "type": "integer" }, "description": "Specific log entry ID" },
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
{ "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20 } },
{ "name": "target_type", "in": "query", "schema": { "type": "string", "enum": ["subscription", "profile"] } },
{ "name": "admin_id", "in": "query", "schema": { "type": "string", "format": "uuid" } },
{ "name": "target_id", "in": "query", "schema": { "type": "string", "format": "uuid" } },
{ "name": "from", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Start date (YYYY-MM-DD)" },
{ "name": "to", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "End date (YYYY-MM-DD)" }
],
"responses": {
"200": {
"description": "Audit log list or detail",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"type": "object",
"properties": {
"logs": { "type": "array", "items": { "$ref": "#/components/schemas/AuditLog" } },
"total": { "type": "integer" },
"page": { "type": "integer" },
"limit": { "type": "integer" }
}
},
{
"type": "object",
"properties": {
"log": { "$ref": "#/components/schemas/AuditLog" },
"admin": { "$ref": "#/components/schemas/Profile" }
}
}
]
}
}
}
}
}
}
}
}
}