d3ro-voice/apps/admin/__tests__/admin-edge.local.integration.mjs
Yun Chan 5a34f66981 feat(admin): 예전/최신 어드민 통합 — 실데이터 복원 + 인증 아키텍처 정리
예전 배포본(HEAD)의 상세 기능을 새 아키텍처(인증=.NET 백엔드, 데이터=Supabase)
위에 실데이터로 복원. 예전 HEAD는 서명 쿠키를 거부하고 위조 쿠키는 통과시키는
인증 결함이 있었고, 삭제된 페이지 다수는 백엔드 호출 0인 하드코딩 목업이었음.

인증/세션
- 로그인 이메일 전용화(username 폐지), 에러 키별 안내 메시지
- ADMIN_COOKIE_SECURE 옵션: TLS 없는 LAN HTTP 배포에서 Secure 쿠키 유실로
  로그인이 유지되지 않던 문제 해결 (login/logout route, admin-session, compose, .env.example)
- Supabase 미설정 시 우아한 저하: isSupabaseAdminConfigured + UnavailableAdminPanel

기능 복원 (실데이터)
- Release Hub: Forgejo API 실데이터(다운로드 수/SHA-256 체크섬/릴리스 이력)
- Ad Monetization: 데스크톱 미디에이션 10개 어댑터 로스터(fail-closed) + ad_reward_claims 통계
- License Issuer: 서버사이드 Ed25519 서명(/api/admin/license, super_admin 전용),
  개인키는 ADMIN_LICENSE_PRIVATE_KEY env로만, 발급 감사를 .NET AdminAuditEntries에 기록
- Service Models: STT 7종/LLM 5종 프리셋 드롭다운 + 자동채움
- 대시보드 ARR/MRR KPI: Supabase 구독 실집계(티어 월단가 기반)
- 사용자 상세 티어별 기능 배지(pro_plus 조건부)

.NET
- SuperAdminOnly 정책 추가, /api/admin/license-audit 엔드포인트, LicenseAuditDto
2026-08-23 23:38:08 +09:00

289 lines
12 KiB
JavaScript

import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { createClient } from '@supabase/supabase-js'
const url = process.env.D3RO_LOCAL_SUPABASE_URL?.trim() ?? ''
const anonKey = process.env.D3RO_LOCAL_SUPABASE_ANON_KEY?.trim() ?? ''
const serviceRoleKey = process.env.D3RO_LOCAL_SUPABASE_SERVICE_ROLE_KEY?.trim() ?? ''
if (!url.startsWith('http://127.0.0.1:55321') || anonKey.length < 20 || serviceRoleKey.length < 20) {
throw new Error('D3RO local Supabase credentials for port 55321 are required')
}
const clientOptions = { auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false } }
const service = createClient(url, serviceRoleKey, clientOptions)
const suffix = randomUUID()
const createdUserIds = []
let assertions = 0
function checked(value, message) {
assert.ok(value, message)
assertions += 1
}
async function createActor(label, role) {
const email = `d3ro.edge.${suffix}.${label}@example.com`
const password = `D3ro-${suffix}-${label}-Strong!`
const { data, error } = await service.auth.admin.createUser({
email,
password,
email_confirm: true,
app_metadata: { role },
user_metadata: { name: `Edge ${label}` }
})
assert.equal(error, null, `${label} creation failed: ${error?.message ?? ''}`)
createdUserIds.push(data.user.id)
const profile = await service.from('profiles').update({ role }).eq('id', data.user.id)
assert.equal(profile.error, null, `${label} profile setup failed`)
const login = createClient(url, anonKey, clientOptions)
const session = await login.auth.signInWithPassword({ email, password })
assert.equal(session.error, null, `${label} sign-in failed`)
return { id: data.user.id, email, token: session.data.session.access_token }
}
async function invoke(functionName, options = {}) {
const headers = { apikey: anonKey, Accept: 'application/json' }
if (options.token) headers.Authorization = `Bearer ${options.token}`
if (options.body !== undefined) headers['Content-Type'] = 'application/json'
if (options.idempotencyKey) headers['Idempotency-Key'] = options.idempotencyKey
const response = await fetch(`${url}/functions/v1/${functionName}${options.query ?? ''}`, {
method: options.method ?? 'GET',
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
})
const text = await response.text()
let payload = null
try {
payload = text ? JSON.parse(text) : null
} catch {
payload = { invalidJson: text }
}
return { status: response.status, payload }
}
function requireStatus(result, status, code, label) {
assert.equal(result.status, status, `${label}: ${JSON.stringify(result.payload)}`)
if (code) assert.equal(result.payload?.error, code, `${label}: wrong stable error code`)
assertions += code ? 2 : 1
}
try {
const manager = await createActor('manager', 'manager')
const staleManager = await createActor('stale-manager', 'manager')
const admin = await createActor('admin', 'admin')
const superAdmin = await createActor('super', 'super_admin')
const target = await createActor('target', 'user')
const anonymous = await invoke('admin-users')
checked(anonymous.status === 401, `anonymous request must be denied, received ${anonymous.status}`)
requireStatus(
await invoke('admin-users', { token: target.token }),
403,
'admin_forbidden',
'ordinary user list access',
)
const staleProfileDemotion = await service.from('profiles').update({ role: 'user' }).eq('id', staleManager.id)
assert.equal(staleProfileDemotion.error, null, 'stale manager profile demotion failed')
const staleAuthDemotion = await service.auth.admin.updateUserById(staleManager.id, { app_metadata: { role: 'user' } })
assert.equal(staleAuthDemotion.error, null, 'stale manager auth demotion failed')
requireStatus(
await invoke('admin-users', { token: staleManager.token }),
403,
'admin_forbidden',
'demoted manager stale token read',
)
requireStatus(
await invoke('admin-users', { token: manager.token, query: '?page=NaN&limit=20' }),
400,
'invalid_pagination',
'NaN pagination',
)
requireStatus(
await invoke('admin-users', { token: manager.token, query: '?debug=true' }),
400,
'unexpected_query_parameter',
'unknown query parameter',
)
const detail = await invoke('admin-users', { token: manager.token, query: `?userId=${target.id}` })
requireStatus(detail, 200, null, 'manager user detail')
checked(detail.payload?.account?.email === target.email, 'user detail must come from the real auth account')
requireStatus(
await invoke('admin-users', {
token: manager.token,
method: 'PATCH',
idempotencyKey: randomUUID(),
body: { userId: target.id, newRole: 'manager', memo: 'manager escalation attempt' },
}),
403,
'admin_forbidden',
'manager role mutation',
)
const roleKey = randomUUID()
const roleBody = { userId: target.id, newRole: 'manager', memo: 'verified support assignment' }
const roleChanged = await invoke('admin-users', {
token: admin.token,
method: 'PATCH',
idempotencyKey: roleKey,
body: roleBody,
})
requireStatus(roleChanged, 200, null, 'admin role mutation')
checked(roleChanged.payload?.newRole === 'manager', 'role response must match the requested role')
const roleReplay = await invoke('admin-users', {
token: admin.token,
method: 'PATCH',
idempotencyKey: roleKey,
body: roleBody,
})
requireStatus(roleReplay, 200, null, 'role idempotent replay')
checked(JSON.stringify(roleReplay.payload) === JSON.stringify(roleChanged.payload), 'idempotent response changed')
const { count: roleAuditCount } = await service.from('audit_log').select('id', { count: 'exact', head: true })
.eq('admin_id', admin.id).eq('action', 'user.role_change').eq('target_id', target.id)
checked(roleAuditCount === 1, 'role replay duplicated audit rows')
requireStatus(
await invoke('admin-users', {
token: admin.token,
method: 'PATCH',
idempotencyKey: roleKey,
body: { ...roleBody, memo: 'different request using the same key' },
}),
409,
'idempotency_conflict',
'role idempotency mismatch',
)
requireStatus(
await invoke('admin-users', {
token: admin.token,
method: 'PATCH',
idempotencyKey: randomUUID(),
body: { userId: target.id, newRole: 'admin', memo: 'unauthorized privileged role assignment' },
}),
403,
'super_admin_required',
'admin privileged role assignment',
)
const superRole = await invoke('admin-users', {
token: superAdmin.token,
method: 'PATCH',
idempotencyKey: randomUUID(),
body: { userId: target.id, newRole: 'admin', memo: 'approved privileged role assignment' },
})
requireStatus(superRole, 200, null, 'super admin privileged role assignment')
const subscriptionRemoval = await service.from('subscriptions').delete().eq('user_id', target.id)
assert.equal(subscriptionRemoval.error, null, 'target subscription fixture removal failed')
const subscriptionKey = randomUUID()
const subscriptionBody = {
userId: target.id,
tier: 'pro',
status: 'active',
overageCredits: 25,
adminNote: 'verified support grant',
memo: 'approved manual subscription grant',
}
const subscriptionCreated = await invoke('admin-subscriptions', {
token: admin.token,
method: 'POST',
idempotencyKey: subscriptionKey,
body: subscriptionBody,
})
requireStatus(subscriptionCreated, 201, null, 'subscription creation')
checked(!JSON.stringify(subscriptionCreated.payload).includes('provider_resource_id'), 'subscription mutation exposed provider resource id')
checked(!JSON.stringify(subscriptionCreated.payload).includes('payple_payer_id'), 'subscription mutation exposed Payple payer id')
const subscriptionReplay = await invoke('admin-subscriptions', {
token: admin.token,
method: 'POST',
idempotencyKey: subscriptionKey,
body: subscriptionBody,
})
requireStatus(subscriptionReplay, 201, null, 'subscription create replay')
checked(JSON.stringify(subscriptionReplay.payload) === JSON.stringify(subscriptionCreated.payload), 'subscription replay response changed')
const updated = await invoke('admin-subscriptions', {
token: manager.token,
method: 'PATCH',
query: `?userId=${target.id}`,
idempotencyKey: randomUUID(),
body: { tier: 'pro_plus', overageCredits: 50, memo: 'verified entitlement correction' },
})
requireStatus(updated, 200, null, 'manager subscription update')
checked(updated.payload?.subscription?.tier === 'pro_plus', 'updated tier must be returned from the RPC')
requireStatus(
await invoke('admin-subscriptions', {
token: manager.token,
method: 'DELETE',
query: `?userId=${target.id}`,
idempotencyKey: randomUUID(),
body: { memo: 'manager delete attempt' },
}),
403,
'admin_forbidden',
'manager subscription delete',
)
requireStatus(
await invoke('admin-subscriptions', {
token: manager.token,
method: 'PATCH',
query: `?userId=${target.id}`,
idempotencyKey: randomUUID(),
body: { overageCredits: -1, memo: 'invalid negative credit update' },
}),
400,
'invalid_overageCredits',
'negative overage credits',
)
const { data: unchanged } = await service.from('subscriptions').select('overage_credits').eq('user_id', target.id).single()
checked(unchanged?.overage_credits === 50, 'failed update changed subscription data')
const paymentHistory = await invoke('admin-payments', { token: manager.token, query: `?userId=${target.id}` })
requireStatus(paymentHistory, 200, null, 'database payment history')
checked(Array.isArray(paymentHistory.payload?.providerEvents), 'provider event ledger must be returned as an array')
checked(!JSON.stringify(paymentHistory.payload).includes('payload_digest'), 'payment payload digest must not be exposed')
checked(!JSON.stringify(paymentHistory.payload).includes('provider_resource_id'), 'provider resource identifier must not be exposed')
requireStatus(
await invoke('admin-payments', { token: manager.token, query: `?userId=${target.id}&source=payple` }),
501,
'payple_live_history_not_configured',
'live Payple history',
)
const sensitiveAudit = await service.from('audit_log').insert({
admin_id: admin.id,
action: 'subscription.redaction_test',
target_type: 'subscription',
target_id: target.id,
before_data: { tier: 'pro', payple_payer_id: 'payer-secret' },
after_data: { tier: 'pro_plus', provider_resource_id: 'resource-secret', payload: { raw: 'provider-payload' } },
memo: 'verify audit response redaction',
})
assert.equal(sensitiveAudit.error, null, 'sensitive audit fixture insertion failed')
const audit = await invoke('admin-audit-log', { token: manager.token, query: `?target_id=${target.id}&limit=100` })
requireStatus(audit, 200, null, 'audit log read')
checked(audit.payload?.logs?.some((entry) => entry.action === 'user.role_change'), 'role audit row is missing')
checked(audit.payload?.logs?.some((entry) => entry.action === 'subscription.update'), 'subscription audit row is missing')
const encodedAudit = JSON.stringify(audit.payload)
checked(!encodedAudit.includes('payer-secret') && !encodedAudit.includes('resource-secret') && !encodedAudit.includes('provider-payload'), 'audit response exposed provider identifiers or payload')
const deleted = await invoke('admin-subscriptions', {
token: admin.token,
method: 'DELETE',
query: `?userId=${target.id}`,
idempotencyKey: randomUUID(),
body: { memo: 'approved subscription retirement' },
})
requireStatus(deleted, 200, null, 'admin subscription soft delete')
checked(deleted.payload?.subscription?.tier === 'free' && deleted.payload?.subscription?.status === 'expired', 'soft delete response is invalid')
console.log(`admin Edge local integration: ${assertions} assertions passed`)
} finally {
if (createdUserIds.length > 0) {
await service.from('audit_log').delete().in('admin_id', createdUserIds)
await service.from('admin_operation_requests').delete().in('actor_id', createdUserIds)
for (const userId of createdUserIds.reverse()) await service.auth.admin.deleteUser(userId)
}
}