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
This commit is contained in:
Yun Chan 2026-08-23 23:38:08 +09:00
parent a9c9a1ca6e
commit 5a34f66981
66 changed files with 4471 additions and 3501 deletions

View file

@ -0,0 +1,289 @@
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)
}
}

View file

@ -0,0 +1,194 @@
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)
}
}

View file

@ -0,0 +1,101 @@
import assert from 'node:assert/strict'
import { chromium } from 'playwright'
const baseUrl = process.env.D3RO_ADMIN_E2E_URL?.trim() ?? ''
if (!baseUrl.startsWith('https://127.0.0.1:')) throw new Error('Local HTTPS admin E2E URL is required')
const browser = await chromium.launch({ headless: true })
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, ignoreHTTPSErrors: true })
await page.addInitScript(() => {
window.alert = () => undefined
window.confirm = () => true
window.prompt = () => 'browser verified endpoint deletion'
})
const consoleErrors = []
const serverErrors = []
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text())
})
page.on('response', (response) => {
if (response.status() >= 500) serverErrors.push(`${response.status()} ${response.url()}`)
})
try {
await page.goto(`${baseUrl}/`, { waitUntil: 'networkidle' })
await page.getByLabel('Admin Identifier (Username / Email)').fill('admin.browser.e2e@example.com')
await page.getByLabel('Password').fill('Admin-browser-e2e-strong-password!')
await Promise.all([
page.waitForURL(`${baseUrl}/`),
page.getByRole('button', { name: 'Sign In as Super Admin' }).click()
])
await page.getByText('BACKEND CONNECTED').waitFor()
const endpointPayload = await page.evaluate(async () => {
const response = await fetch('/api/admin/backend/endpoints', { cache: 'no-store' })
return { status: response.status, body: await response.json() }
})
assert.equal(endpointPayload.status, 200)
assert.ok(Array.isArray(endpointPayload.body))
assert.ok(endpointPayload.body.every((endpoint) => endpoint.apiKey === '' || endpoint.apiKey === '••••••••'))
await page.goto(`${baseUrl}/models`, { waitUntil: 'networkidle' })
await page.getByRole('tab', { name: /LLM & Reasoning Model Endpoints/ }).click()
const addModelButton = page.getByRole('button', { name: /Add LLM Model Endpoint/ })
if (await addModelButton.count() === 0) {
const cookies = await page.context().cookies()
const bodyText = (await page.locator('body').innerText()).slice(0, 1200)
throw new Error(`models diagnostic url=${page.url()} cookies=${cookies.map((cookie) => `${cookie.name}:${cookie.secure}:${cookie.sameSite}`).join(',')} body=${bodyText}`)
}
await addModelButton.click()
await page.getByLabel('Model ID').fill('browser-e2e-model')
await page.getByLabel('Display Name').fill('Browser E2E Model')
await page.getByLabel('Endpoint URL').fill('https://models.example.com/v1')
await page.getByLabel('Audit memo').last().fill('browser verified endpoint creation')
const createResponse = page.waitForResponse((response) => response.url().includes('/api/admin/backend/endpoints') && response.request().method() === 'POST')
await page.getByRole('button', { name: 'Save Endpoint' }).click()
const created = await createResponse
assert.equal(created.status(), 200, `model creation failed: ${await created.text()}`)
await page.getByText('Browser E2E Model').waitFor()
const modelRow = page.locator('tr').filter({ hasText: 'browser-e2e-model' })
const deleteResponse = page.waitForResponse((response) => response.url().includes('/api/admin/backend/endpoints/') && response.request().method() === 'DELETE')
await modelRow.getByRole('button', { name: 'Delete' }).click()
const deleted = await deleteResponse
assert.equal(deleted.status(), 200, `model deletion failed: ${await deleted.text()}`)
await page.getByText('Browser E2E Model').waitFor({ state: 'detached' })
await page.goto(`${baseUrl}/users`, { waitUntil: 'networkidle' })
const customerRow = page.locator('tr').filter({ hasText: 'customer.browser.e2e@example.com' })
await customerRow.waitFor()
const customerHref = await customerRow.getByRole('link').first().getAttribute('href')
assert.match(customerHref ?? '', /^\/users\/[0-9a-f-]{36}$/)
const customerId = customerHref.split('/').pop()
await page.goto(`${baseUrl}${customerHref}`, { waitUntil: 'networkidle' })
await page.getByRole('button', { name: 'Change Role' }).click()
const roleDialog = page.getByRole('dialog', { name: /CHANGE USER ROLE/ })
await roleDialog.waitFor()
await roleDialog.getByRole('combobox').click()
await page.getByRole('option', { name: 'manager' }).click()
await page.getByPlaceholder('Reason for role change (required)...').fill('browser verified support assignment')
await page.getByRole('button', { name: 'Change Role', exact: true }).last().click()
await page.getByText('MANAGER').first().waitFor()
await page.goto(`${baseUrl}/subscriptions/${customerId}`, { waitUntil: 'networkidle' })
await page.getByRole('combobox', { name: 'Tier' }).click()
await page.getByRole('option', { name: 'PRO', exact: true }).click()
await page.getByLabel('Memo (required for audit log)').fill('browser verified subscription update')
await page.getByRole('button', { name: 'Update' }).click()
await page.getByText('Operation successful').waitFor()
await page.goto(`${baseUrl}/audit-log`, { waitUntil: 'networkidle' })
await page.getByText('user.role_change').first().waitFor()
await page.getByText('subscription.update').first().waitFor()
await page.screenshot({ path: 'scratch/admin-browser-e2e.png', fullPage: true })
assert.deepEqual(serverErrors, [], `server errors: ${serverErrors.join(', ')}`)
assert.deepEqual(consoleErrors, [], `console errors: ${consoleErrors.join(', ')}`)
console.log('admin browser E2E: login, JWT proxy, model create/delete, users, role, subscription, audit GREEN')
} finally {
await browser.close()
}

View file

@ -0,0 +1,57 @@
import { createClient } from '@supabase/supabase-js'
const action = process.argv[2]
const url = process.env.D3RO_LOCAL_SUPABASE_URL?.trim() ?? ''
const serviceRoleKey = process.env.D3RO_LOCAL_SUPABASE_SERVICE_ROLE_KEY?.trim() ?? ''
if (!url.startsWith('http://127.0.0.1:55321') || serviceRoleKey.length < 20) {
throw new Error('D3RO local Supabase service credentials are required')
}
const client = createClient(url, serviceRoleKey, {
auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false }
})
const adminEmail = 'admin.browser.e2e@example.com'
const targetEmail = 'customer.browser.e2e@example.com'
async function matchingUsers() {
const { data, error } = await client.auth.admin.listUsers({ page: 1, perPage: 1000 })
if (error) throw error
return data.users.filter((user) => user.email === adminEmail || user.email === targetEmail)
}
async function cleanup() {
const users = await matchingUsers()
const ids = users.map((user) => user.id)
if (ids.length === 0) return
await client.from('audit_log').delete().in('admin_id', ids)
await client.from('admin_operation_requests').delete().in('actor_id', ids)
for (const user of users) await client.auth.admin.deleteUser(user.id)
}
if (action === 'cleanup') {
await cleanup()
console.log('admin browser fixture cleaned')
} else if (action === 'setup') {
await cleanup()
const admin = await client.auth.admin.createUser({
email: adminEmail,
password: 'Admin-browser-e2e-strong-password!',
email_confirm: true,
app_metadata: { role: 'super_admin' },
user_metadata: { name: 'Browser E2E Admin' }
})
if (admin.error) throw admin.error
const target = await client.auth.admin.createUser({
email: targetEmail,
password: 'Customer-browser-e2e-strong-password!',
email_confirm: true,
app_metadata: { role: 'user' },
user_metadata: { name: 'Browser E2E Customer' }
})
if (target.error) throw target.error
const roleUpdate = await client.from('profiles').update({ role: 'super_admin' }).eq('id', admin.data.user.id)
if (roleUpdate.error) throw roleUpdate.error
console.log(`admin browser fixture ready target=${target.data.user.id}`)
} else {
throw new Error('Expected setup or cleanup')
}