예전 배포본(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
194 lines
8.7 KiB
JavaScript
194 lines
8.7 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 options = { auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false } }
|
|
const service = createClient(url, serviceRoleKey, options)
|
|
const anonymous = createClient(url, anonKey, options)
|
|
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.admin.${suffix}.${label}@example.com`
|
|
const { data, error } = await service.auth.admin.createUser({
|
|
email,
|
|
password: `D3ro-${suffix}-${label}-Strong!`,
|
|
email_confirm: true,
|
|
app_metadata: { role },
|
|
user_metadata: { name: label }
|
|
})
|
|
assert.equal(error, null, `${label} creation failed: ${error?.message ?? ''}`)
|
|
createdUserIds.push(data.user.id)
|
|
const profileUpdate = await service.from('profiles').update({ role }).eq('id', data.user.id)
|
|
assert.equal(profileUpdate.error, null, `${label} profile role setup failed`)
|
|
return { id: data.user.id, email }
|
|
}
|
|
|
|
function requireSuccess(result, label) {
|
|
assert.equal(result.error, null, `${label}: ${result.error?.message ?? 'unknown error'}`)
|
|
checked(result.data?.success === true, `${label}: success response missing`)
|
|
return result.data
|
|
}
|
|
|
|
function requireFailure(result, pattern, label) {
|
|
assert.ok(result.error, `${label}: unexpectedly succeeded`)
|
|
assert.match(`${result.error.code ?? ''} ${result.error.message ?? ''}`, pattern, `${label}: wrong error`)
|
|
assertions += 1
|
|
}
|
|
|
|
try {
|
|
const superAdmin = await createActor('super', 'super_admin')
|
|
const admin = await createActor('admin', 'admin')
|
|
const manager = await createActor('manager', 'manager')
|
|
const target = await createActor('target', 'user')
|
|
|
|
requireFailure(await anonymous.rpc('admin_change_user_role_v1', {
|
|
p_actor_email: admin.email,
|
|
p_idempotency_key: randomUUID(),
|
|
p_target_user_id: target.id,
|
|
p_new_role: 'manager',
|
|
p_memo: 'anonymous bypass attempt'
|
|
}), /permission denied|42501/i, 'anonymous RPC execution')
|
|
|
|
requireFailure(await service.rpc('admin_change_user_role_v1', {
|
|
p_actor_email: manager.email,
|
|
p_idempotency_key: randomUUID(),
|
|
p_target_user_id: target.id,
|
|
p_new_role: 'manager',
|
|
p_memo: 'manager escalation attempt'
|
|
}), /insufficient_admin_role|42501/i, 'manager role mutation')
|
|
|
|
const roleKey = randomUUID()
|
|
const roleArgs = {
|
|
p_actor_email: admin.email,
|
|
p_idempotency_key: roleKey,
|
|
p_target_user_id: target.id,
|
|
p_new_role: 'manager',
|
|
p_memo: 'verified customer support assignment'
|
|
}
|
|
const roleChanged = requireSuccess(await service.rpc('admin_change_user_role_v1', roleArgs), 'admin role change')
|
|
const roleReplayed = requireSuccess(await service.rpc('admin_change_user_role_v1', roleArgs), 'role idempotent replay')
|
|
checked(JSON.stringify(roleChanged) === JSON.stringify(roleReplayed), 'role replay response changed')
|
|
|
|
const [{ data: targetProfile }, { data: targetAuth }, { count: roleAuditCount }] = await Promise.all([
|
|
service.from('profiles').select('role').eq('id', target.id).single(),
|
|
service.auth.admin.getUserById(target.id),
|
|
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(targetProfile?.role === 'manager', 'profile role was not updated')
|
|
checked(targetAuth?.user?.app_metadata?.role === 'manager', 'auth app_metadata role was not updated')
|
|
checked(roleAuditCount === 1, 'idempotent role replay duplicated audit rows')
|
|
|
|
requireFailure(await service.rpc('admin_change_user_role_v1', {
|
|
...roleArgs,
|
|
p_memo: 'different request with reused key'
|
|
}), /idempotency_key_reused|22023/i, 'role idempotency mismatch')
|
|
|
|
requireFailure(await service.rpc('admin_change_user_role_v1', {
|
|
p_actor_email: admin.email,
|
|
p_idempotency_key: randomUUID(),
|
|
p_target_user_id: target.id,
|
|
p_new_role: 'admin',
|
|
p_memo: 'unauthorized privileged assignment'
|
|
}), /super_admin_required|42501/i, 'admin privileged role assignment')
|
|
|
|
requireSuccess(await service.rpc('admin_change_user_role_v1', {
|
|
p_actor_email: superAdmin.email,
|
|
p_idempotency_key: randomUUID(),
|
|
p_target_user_id: target.id,
|
|
p_new_role: 'admin',
|
|
p_memo: 'approved privileged assignment'
|
|
}), 'super admin privileged assignment')
|
|
|
|
await service.from('subscriptions').delete().eq('user_id', target.id)
|
|
const subscriptionKey = randomUUID()
|
|
const createArgs = {
|
|
p_actor_email: admin.email,
|
|
p_idempotency_key: subscriptionKey,
|
|
p_action: 'create',
|
|
p_user_id: target.id,
|
|
p_tier: 'pro',
|
|
p_status: 'active',
|
|
p_overage_credits: 25,
|
|
p_admin_note: 'verified support grant',
|
|
p_memo: 'approved manual subscription grant'
|
|
}
|
|
const createdSubscription = requireSuccess(await service.rpc('admin_mutate_subscription_v1', createArgs), 'subscription create')
|
|
checked(!JSON.stringify(createdSubscription).includes('provider_resource_id'), 'subscription RPC exposed provider resource id')
|
|
checked(!JSON.stringify(createdSubscription).includes('payple_payer_id'), 'subscription RPC exposed Payple payer id')
|
|
requireSuccess(await service.rpc('admin_mutate_subscription_v1', createArgs), 'subscription create replay')
|
|
const { count: createAuditCount } = await service.from('audit_log').select('id', { count: 'exact', head: true })
|
|
.eq('admin_id', admin.id).eq('action', 'subscription.create').eq('target_id', target.id)
|
|
checked(createAuditCount === 1, 'subscription replay duplicated audit rows')
|
|
|
|
requireSuccess(await service.rpc('admin_mutate_subscription_v1', {
|
|
p_actor_email: manager.email,
|
|
p_idempotency_key: randomUUID(),
|
|
p_action: 'update',
|
|
p_user_id: target.id,
|
|
p_tier: 'pro_plus',
|
|
p_status: 'active',
|
|
p_overage_credits: 50,
|
|
p_memo: 'customer support entitlement correction'
|
|
}), 'manager subscription update')
|
|
const [{ data: updatedSubscription }, { data: updatedProfile }] = await Promise.all([
|
|
service.from('subscriptions').select('tier, overage_credits').eq('user_id', target.id).single(),
|
|
service.from('profiles').select('tier').eq('id', target.id).single()
|
|
])
|
|
checked(updatedSubscription?.tier === 'pro_plus' && updatedSubscription?.overage_credits === 50, 'subscription update was not persisted')
|
|
checked(updatedProfile?.tier === 'pro_plus', 'profile tier was not synchronized')
|
|
|
|
requireFailure(await service.rpc('admin_mutate_subscription_v1', {
|
|
p_actor_email: manager.email,
|
|
p_idempotency_key: randomUUID(),
|
|
p_action: 'delete',
|
|
p_user_id: target.id,
|
|
p_memo: 'manager delete attempt'
|
|
}), /admin_role_required|42501/i, 'manager subscription delete')
|
|
|
|
requireFailure(await service.rpc('admin_mutate_subscription_v1', {
|
|
p_actor_email: admin.email,
|
|
p_idempotency_key: randomUUID(),
|
|
p_action: 'update',
|
|
p_user_id: target.id,
|
|
p_overage_credits: -1,
|
|
p_memo: 'invalid negative credit mutation'
|
|
}), /invalid_overage_credits|22023/i, 'negative credits validation')
|
|
const { data: unchanged } = await service.from('subscriptions').select('overage_credits').eq('user_id', target.id).single()
|
|
checked(unchanged?.overage_credits === 50, 'failed mutation changed subscription data')
|
|
|
|
requireSuccess(await service.rpc('admin_mutate_subscription_v1', {
|
|
p_actor_email: admin.email,
|
|
p_idempotency_key: randomUUID(),
|
|
p_action: 'delete',
|
|
p_user_id: target.id,
|
|
p_memo: 'approved subscription retirement'
|
|
}), 'admin subscription soft delete')
|
|
const [{ data: deletedSubscription }, { data: deletedProfile }] = await Promise.all([
|
|
service.from('subscriptions').select('tier, status').eq('user_id', target.id).single(),
|
|
service.from('profiles').select('tier').eq('id', target.id).single()
|
|
])
|
|
checked(deletedSubscription?.tier === 'free' && deletedSubscription?.status === 'expired', 'soft delete state is invalid')
|
|
checked(deletedProfile?.tier === 'free', 'soft delete did not synchronize profile tier')
|
|
|
|
console.log(`admin management 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)
|
|
}
|
|
}
|