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() 선행 토큰 갱신)
This commit is contained in:
윤찬 2026-04-13 01:28:10 +09:00
parent d0e854c255
commit 8af75a0a1e
50 changed files with 2185 additions and 950 deletions

View file

@ -3,7 +3,7 @@
import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts'
import { authErrorResponse, type AuthError } from '../_shared/auth.ts'
import { requireAdmin, requireSuperAdmin } from '../_shared/admin-auth.ts'
import { requireManager, requireAdmin, requireSuperAdmin, hasMinRole } from '../_shared/admin-auth.ts'
import { createServiceRoleClient } from '../_shared/quota.ts'
import { writeAuditLog } from '../_shared/audit.ts'
@ -19,7 +19,7 @@ function jsonResponse(body: Record<string, unknown> | Record<string, unknown>[],
interface RoleChangeBody {
userId: string
newRole: 'user' | 'admin' | 'super_admin'
newRole: 'user' | 'manager' | 'admin' | 'super_admin'
memo: string
}
@ -31,9 +31,9 @@ Deno.serve(async (req: Request) => {
try {
const url = new URL(req.url)
// ── GET: 유저 목록/상세 ──
// ── GET: 유저 목록/상세 (manager 이상) ──
if (req.method === 'GET') {
const admin = await requireAdmin(req)
const admin = await requireManager(req)
const serviceClient = createServiceRoleClient()
const userId = url.searchParams.get('userId')
@ -86,9 +86,11 @@ Deno.serve(async (req: Request) => {
return jsonResponse({ profiles: profiles ?? [], total: count ?? 0, page, limit })
}
// ── PATCH: role 변경 (super_admin 전용) ──
// ── PATCH: role 변경 ──
// admin: user↔manager 변경 가능
// super_admin: 모든 role 변경 가능 (→admin 포함)
if (req.method === 'PATCH') {
const admin = await requireSuperAdmin(req)
const admin = await requireAdmin(req)
const body = (await req.json()) as RoleChangeBody
const serviceClient = createServiceRoleClient()
@ -96,11 +98,16 @@ Deno.serve(async (req: Request) => {
return jsonResponse({ error: 'userId, newRole, memo are required' }, 400)
}
const validRoles = ['user', 'admin', 'super_admin']
const validRoles = ['user', 'manager', 'admin', 'super_admin']
if (!validRoles.includes(body.newRole)) {
return jsonResponse({ error: `Invalid role. Must be one of: ${validRoles.join(', ')}` }, 400)
}
// admin은 user↔manager만 변경 가능, admin/super_admin 변경은 super_admin만
if (!hasMinRole(admin, 'super_admin') && (body.newRole === 'admin' || body.newRole === 'super_admin')) {
return jsonResponse({ error: 'Only super_admin can assign admin or super_admin roles' }, 403)
}
// 현재 프로필 조회 (before 스냅샷)
const { data: before } = await serviceClient
.from('profiles')
@ -110,6 +117,12 @@ Deno.serve(async (req: Request) => {
if (!before) return jsonResponse({ error: 'User not found' }, 404)
// admin이 admin/super_admin 유저의 role을 변경하려는 시도 차단
const targetRole = (before as Record<string, unknown>).role as string
if (!hasMinRole(admin, 'super_admin') && (targetRole === 'admin' || targetRole === 'super_admin')) {
return jsonResponse({ error: 'Only super_admin can modify admin or super_admin users' }, 403)
}
// 1. auth.users.raw_app_meta_data.role 변경
const { error: authError } = await serviceClient.auth.admin.updateUserById(body.userId, {
app_metadata: { role: body.newRole },