'use client' // apps/admin/src/components/license-issuer-dialog.tsx // Ed25519 라이선스 발급 다이얼로그 — 서명은 서버(/api/admin/license)에서만 수행한다. import { useState } from 'react' import { Dialog, DialogTitle, DialogContent, Box, Typography, IconButton, TextField, FormControl, InputLabel, Select, MenuItem, Button, Alert } from '@mui/material' import type { LicenseTier } from '@d3ro/core/types' import { PLAN_PRICE_KRW } from '@d3ro/core/plan-catalog' import { d3roPalette, d3roFontMono, d3roRadius } from '@d3ro/ui/theme' import { C, FONT_SANS, FONT_MONO, primaryButtonSx } from '@/lib/console-theme' const krw = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW', maximumFractionDigits: 0 }) interface LicenseIssuerDialogProps { open: boolean onClose: () => void } function describeIssueError(errorKey: unknown): string { if (errorKey === 'license_signing_unavailable') { return '서명 키가 구성되지 않았습니다. ADMIN_LICENSE_PRIVATE_KEY 환경변수를 설정해주세요.' } if (errorKey === 'admin_forbidden') return 'super_admin 권한이 필요한 작업입니다.' if (errorKey === 'admin_session_invalid') return '세션이 만료되었습니다. 다시 로그인해주세요.' if (errorKey === 'invalid_customer_email') return '고객 이메일 형식이 올바르지 않습니다.' return '라이선스 발급에 실패했습니다.' } export function LicenseIssuerDialog({ open, onClose }: LicenseIssuerDialogProps): React.ReactElement { const [customerEmail, setCustomerEmail] = useState('') const [tier, setTier] = useState('pro_plus') const [validity, setValidity] = useState<'30d' | '365d' | 'lifetime'>('365d') const [machineId, setMachineId] = useState('') const [teamId, setTeamId] = useState('') const [generatedKey, setGeneratedKey] = useState(null) const [auditRecorded, setAuditRecorded] = useState(true) const [loading, setLoading] = useState(false) const [copied, setCopied] = useState(false) const [error, setError] = useState(null) const handleGenerate = async (): Promise => { setError(null) setCopied(false) if (!customerEmail.trim()) { setError('Customer Email is required') return } setLoading(true) try { const response = await fetch('/api/admin/license', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customerEmail: customerEmail.trim(), tier, validity, machineId: machineId.trim(), teamId: teamId.trim() }) }) const data = (await response.json()) as Record if (!response.ok || data.success !== true || typeof data.licenseKey !== 'string') { throw new Error(describeIssueError(data.error)) } setGeneratedKey(data.licenseKey) setAuditRecorded(data.auditRecorded !== false) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to generate license key') } finally { setLoading(false) } } const handleCopy = (): void => { if (!generatedKey) return void navigator.clipboard.writeText(generatedKey) setCopied(true) setTimeout(() => setCopied(false), 3000) } const inputSx = { '& .MuiInputBase-root': { fontFamily: d3roFontMono, fontSize: 13, color: d3roPalette.text.primary, bgcolor: d3roPalette.bg.inset, borderRadius: d3roRadius.button }, '& .MuiInputLabel-root': { fontFamily: FONT_MONO, fontSize: 12, color: C.dim } } return ( Issue Cryptographic License Key ED25519 SERVER-SIGNED OFFLINE / ENTERPRISE TOKEN ✕ setCustomerEmail(e.target.value)} sx={inputSx} /> Plan / Tier Validity Period {(tier === 'team' || tier === 'enterprise') && ( setTeamId(e.target.value)} sx={inputSx} /> )} setMachineId(e.target.value)} sx={inputSx} /> {error && {error}} {generatedKey && ( {!auditRecorded && ( 라이선스는 발급되었으나 감사 로그 기록에 실패했습니다. 백엔드 연결을 확인하세요. )} SIGNED LICENSE KEY (Copy and paste into D3RO Voice Desktop App): {generatedKey} )} ) }