feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# apps/admin/Dockerfile
|
||||
FROM node:24-alpine AS base
|
||||
FROM node:24.19.0-alpine AS base
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@d3ro/admin",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"description": "D3RO Voice Admin CRM — SaaS 관리 도구",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// D3RO Voice Admin CRM — Release & Distribution Hub (Forgejo live feed)
|
||||
|
||||
import { Box, Typography, Button } from '@mui/material'
|
||||
import Link from 'next/link'
|
||||
import { DoubleBezelCard, TactileBadge } from '@d3ro/ui/components/ds'
|
||||
import { C, FONT_SANS, FONT_MONO, panelSx, tableSx, statusBadgeSx } from '@/lib/console-theme'
|
||||
import { requireManager } from '@/lib/admin-guard'
|
||||
|
|
@ -91,7 +90,6 @@ function HeaderBar({ feedLive, repoHtmlUrl }: { feedLive: boolean; repoHtmlUrl:
|
|||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
component={Link}
|
||||
href={`${repoHtmlUrl}/releases`}
|
||||
target="_blank"
|
||||
variant="outlined"
|
||||
|
|
@ -109,7 +107,6 @@ function HeaderBar({ feedLive, repoHtmlUrl }: { feedLive: boolean; repoHtmlUrl:
|
|||
Forgejo Releases ↗
|
||||
</Button>
|
||||
<Button
|
||||
component={Link}
|
||||
href="https://d3ro.chanpaca.net/download.html"
|
||||
target="_blank"
|
||||
variant="outlined"
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@
|
|||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
issueSignedLicenseKey,
|
||||
DEFAULT_LICENSE_PRIVATE_KEY,
|
||||
type SignedLicensePayload
|
||||
} from '@d3ro/core/utils/crypto-license'
|
||||
import { issueSignedLicenseKey, type SignedLicensePayload } from '@d3ro/core/utils/crypto-license'
|
||||
import type { LicenseTier } from '@d3ro/core/types'
|
||||
import { AdminBackendError, fetchAdminBackend, requireVerifiedBackendSession } from '@/lib/backend-session'
|
||||
|
||||
|
|
@ -27,20 +23,11 @@ const MAX_DEVICES: Record<LicenseTier, number> = {
|
|||
enterprise: 999
|
||||
}
|
||||
|
||||
interface SigningKey {
|
||||
privateKeyPem: string
|
||||
usedDefaultKey: boolean
|
||||
}
|
||||
|
||||
// 저장소에 포함된 기본 키쌍은 공개된 것이므로 위조 방어력이 없다. 운영 발급은
|
||||
// ADMIN_LICENSE_PRIVATE_KEY(전용 키 로테이션)로만 하고, 기본 키 사용은 명시적 opt-in.
|
||||
function resolveSigningKey(): SigningKey | null {
|
||||
// 공개된 개발 키로의 fallback은 없다. 모든 환경에서 명시적인 전용 서명키가 필요하다.
|
||||
function resolveSigningKey(): string | null {
|
||||
const configured = process.env.ADMIN_LICENSE_PRIVATE_KEY?.trim()
|
||||
if (configured) {
|
||||
return { privateKeyPem: configured.replace(/\\n/g, '\n'), usedDefaultKey: false }
|
||||
}
|
||||
if (process.env.ADMIN_LICENSE_ALLOW_DEFAULT_KEY === 'true') {
|
||||
return { privateKeyPem: DEFAULT_LICENSE_PRIVATE_KEY, usedDefaultKey: true }
|
||||
return configured.replace(/\\n/g, '\n')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -119,7 +106,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|||
|
||||
let licenseKey: string
|
||||
try {
|
||||
licenseKey = issueSignedLicenseKey(payload, signingKey.privateKeyPem)
|
||||
licenseKey = issueSignedLicenseKey(payload, signingKey)
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'license_signing_failed' }, { status: 500 })
|
||||
}
|
||||
|
|
@ -151,7 +138,6 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|||
licenseKey,
|
||||
licenseId: payload.licenseId,
|
||||
expiresAt: payload.expiresAt,
|
||||
usedDefaultKey: signingKey.usedDefaultKey,
|
||||
auditRecorded
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ export function LicenseIssuerDialog({ open, onClose }: LicenseIssuerDialogProps)
|
|||
const [machineId, setMachineId] = useState('')
|
||||
const [teamId, setTeamId] = useState('')
|
||||
const [generatedKey, setGeneratedKey] = useState<string | null>(null)
|
||||
const [usedDefaultKey, setUsedDefaultKey] = useState(false)
|
||||
const [auditRecorded, setAuditRecorded] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
|
@ -77,7 +76,6 @@ export function LicenseIssuerDialog({ open, onClose }: LicenseIssuerDialogProps)
|
|||
throw new Error(describeIssueError(data.error))
|
||||
}
|
||||
setGeneratedKey(data.licenseKey)
|
||||
setUsedDefaultKey(data.usedDefaultKey === true)
|
||||
setAuditRecorded(data.auditRecorded !== false)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to generate license key')
|
||||
|
|
@ -211,12 +209,6 @@ export function LicenseIssuerDialog({ open, onClose }: LicenseIssuerDialogProps)
|
|||
|
||||
{generatedKey && (
|
||||
<Box sx={{ mt: 1, p: 2, bgcolor: 'rgba(0, 0, 0, 0.4)', borderRadius: '10px', border: `1px solid ${C.borderStrong}` }}>
|
||||
{usedDefaultKey && (
|
||||
<Alert severity="warning" sx={{ fontFamily: FONT_MONO, fontSize: '11px', mb: 1.5 }}>
|
||||
저장소 기본 키로 서명되었습니다. 기본 키쌍은 공개되어 위조 방어력이 없으므로 운영 배포 전
|
||||
ADMIN_LICENSE_PRIVATE_KEY로 키를 로테이션하세요.
|
||||
</Alert>
|
||||
)}
|
||||
{!auditRecorded && (
|
||||
<Alert severity="info" sx={{ fontFamily: FONT_MONO, fontSize: '11px', mb: 1.5 }}>
|
||||
라이선스는 발급되었으나 감사 로그 기록에 실패했습니다. 백엔드 연결을 확인하세요.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue