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:
parent
a9c9a1ca6e
commit
5a34f66981
66 changed files with 4471 additions and 3501 deletions
|
|
@ -14,8 +14,6 @@ interface NavGroup {
|
|||
key: string
|
||||
path: string
|
||||
label: string
|
||||
badge?: string
|
||||
badgeColor?: 'blue' | 'purple' | 'green' | 'orange'
|
||||
icon: React.ReactElement
|
||||
}>
|
||||
}
|
||||
|
|
@ -38,8 +36,6 @@ const NAV_GROUPS: NavGroup[] = [
|
|||
key: 'pipelines',
|
||||
path: '/pipelines',
|
||||
label: 'AI & Voice Pipelines',
|
||||
badge: 'v0.2',
|
||||
badgeColor: 'blue',
|
||||
icon: (
|
||||
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 100-6 3 3 0 000 6z" />
|
||||
|
|
@ -60,8 +56,6 @@ const NAV_GROUPS: NavGroup[] = [
|
|||
key: 'releases',
|
||||
path: '/releases',
|
||||
label: 'Release & Downloads',
|
||||
badge: 'v1.0',
|
||||
badgeColor: 'green',
|
||||
icon: (
|
||||
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
|
|
@ -77,8 +71,6 @@ const NAV_GROUPS: NavGroup[] = [
|
|||
key: 'users',
|
||||
path: '/users',
|
||||
label: 'User Directory',
|
||||
badge: '4.5k',
|
||||
badgeColor: 'purple',
|
||||
icon: (
|
||||
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
|
|
@ -99,8 +91,6 @@ const NAV_GROUPS: NavGroup[] = [
|
|||
key: 'ads',
|
||||
path: '/ads',
|
||||
label: 'Ad Monetization',
|
||||
badge: '$4.6k',
|
||||
badgeColor: 'blue',
|
||||
icon: (
|
||||
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
|
|
@ -111,8 +101,6 @@ const NAV_GROUPS: NavGroup[] = [
|
|||
key: 'support',
|
||||
path: '/support',
|
||||
label: 'Customer Support (CA)',
|
||||
badge: '4 Live',
|
||||
badgeColor: 'orange',
|
||||
icon: (
|
||||
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
|
|
@ -148,7 +136,9 @@ const NAV_GROUPS: NavGroup[] = [
|
|||
},
|
||||
]
|
||||
|
||||
export function AdminSidebar(): React.ReactElement {
|
||||
export function AdminSidebar({ identity }: {
|
||||
identity: { email: string; role: 'manager' | 'admin' | 'super_admin' }
|
||||
}): React.ReactElement {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
|
|
@ -239,7 +229,7 @@ export function AdminSidebar(): React.ReactElement {
|
|||
sx={{
|
||||
fontFamily: FONT_SANS,
|
||||
fontSize: '15px',
|
||||
fontWeight: 700,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '-0.02em',
|
||||
color: C.bright,
|
||||
lineHeight: 1.2,
|
||||
|
|
@ -261,7 +251,7 @@ export function AdminSidebar(): React.ReactElement {
|
|||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Live Node Pulse */}
|
||||
{/* Verified local session indicator */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
|
|
@ -285,7 +275,7 @@ export function AdminSidebar(): React.ReactElement {
|
|||
}}
|
||||
/>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', fontWeight: 600, color: '#34d399' }}>
|
||||
LIVE
|
||||
SESSION
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -377,63 +367,12 @@ export function AdminSidebar(): React.ReactElement {
|
|||
</Typography>
|
||||
</Box>
|
||||
|
||||
{item.badge && (
|
||||
<Box
|
||||
sx={{
|
||||
px: 0.9,
|
||||
py: 0.2,
|
||||
borderRadius: '999px',
|
||||
fontSize: '10px',
|
||||
fontFamily: FONT_MONO,
|
||||
fontWeight: 700,
|
||||
bgcolor: item.badgeColor === 'blue' ? 'rgba(59, 130, 246, 0.2)' : 'rgba(139, 92, 246, 0.2)',
|
||||
color: item.badgeColor === 'blue' ? C.cyanLight : C.purple400,
|
||||
border: `1px solid ${item.badgeColor === 'blue' ? 'rgba(59, 130, 246, 0.3)' : 'rgba(139, 92, 246, 0.3)'}`,
|
||||
}}
|
||||
>
|
||||
{item.badge}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/* Live Service Matrix Mini-Widget */}
|
||||
<Box
|
||||
sx={{
|
||||
mt: 1,
|
||||
p: 2,
|
||||
borderRadius: '14px',
|
||||
bgcolor: 'rgba(13, 21, 38, 0.7)',
|
||||
border: `1px solid ${C.border}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1.2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '11px', fontWeight: 600, color: C.dim, textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
||||
Service Telemetry
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: '#10b981' }}>
|
||||
99.9% Up
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1 }}>
|
||||
<Box sx={{ p: 1, borderRadius: '8px', bgcolor: 'rgba(17, 26, 48, 0.6)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', color: C.dim }}>STT LATENCY</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '12px', fontWeight: 700, color: C.cyanLight }}>142ms</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 1, borderRadius: '8px', bgcolor: 'rgba(17, 26, 48, 0.6)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', color: C.dim }}>OLLAMA VRAM</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '12px', fontWeight: 700, color: C.purple400 }}>4.6 GB</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* User Footer */}
|
||||
|
|
@ -471,20 +410,20 @@ export function AdminSidebar(): React.ReactElement {
|
|||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 700,
|
||||
fontWeight: 500,
|
||||
fontSize: '13px',
|
||||
color: '#ffffff',
|
||||
boxShadow: '0 0 12px rgba(59, 130, 246, 0.4)',
|
||||
}}
|
||||
>
|
||||
A
|
||||
{identity.email.slice(0, 1).toUpperCase()}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', fontWeight: 600, color: C.bright, lineHeight: 1.2 }}>
|
||||
Admin User
|
||||
{identity.email}
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', fontWeight: 600, color: C.purple400, letterSpacing: '0.04em' }}>
|
||||
SUPER_ADMIN
|
||||
{identity.role.toUpperCase()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -516,4 +455,3 @@ export function AdminSidebar(): React.ReactElement {
|
|||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue