2026-06-02 19:25:48 +08:00

204 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from 'next/server';
import { generateFingerprint } from '@/lib/license/fingerprint-service';
import { verifyLicenseSignature } from '@/lib/license/crypto-service';
import { saveLicenseLocally, initializeSystem, startHeartbeat } from '@/lib/license/license-service';
import { buildLicenseServerLicenseApiUrl } from '@/lib/license/server-config';
import { buildSignedLicenseRequest } from '@/lib/license/request-signature';
import { syncPremiumModules } from '@/lib/license/premium-module-service';
import { requireSuperAdminAfterInitialSetup } from '@/lib/license/license-route-guards';
import { validateSignedLicenseBinding } from '@/lib/license/license-binding';
function maskSecret(value: unknown): string {
if (typeof value !== 'string' || !value) return '****';
if (value.length <= 8) return '****';
return `${value.slice(0, 4)}****${value.slice(-4)}`;
}
function sanitizeLicenseServerLogPayload(payload: unknown) {
if (!payload || typeof payload !== 'object') return payload;
const raw = payload as Record<string, any>;
return {
...raw,
data: raw.data
? {
...raw.data,
activationCode: raw.data.activationCode ? maskSecret(raw.data.activationCode) : raw.data.activationCode,
}
: raw.data,
license: raw.license
? {
data: raw.license.data ? '[present]' : raw.license.data,
signature: raw.license.signature ? '[redacted]' : raw.license.signature,
}
: raw.license,
};
}
export async function POST(request: NextRequest) {
try {
const guard = await requireSuperAdminAfterInitialSetup();
if (!guard.allowed) {
return NextResponse.json(
{ success: false, error: guard.error },
{ status: guard.status }
);
}
const body = await request.json().catch(() => ({}));
const customerName = body.customerName?.trim();
const contactInfo = body.contactInfo?.trim();
// 验证必填字段
if (!customerName || customerName.length === 0) {
return NextResponse.json(
{ success: false, error: '客户名称不能为空' },
{ status: 400 }
);
}
if (!contactInfo || contactInfo.length === 0) {
return NextResponse.json(
{ success: false, error: '联系方式不能为空' },
{ status: 400 }
);
}
// ✅ 安全改进:在后端生成硬件指纹(不暴露给客户端)
const fingerprint = await generateFingerprint();
// 确保指纹有效
if (!fingerprint.machineId || !fingerprint.instanceId) {
console.error('[License] Fingerprint验证失败:', fingerprint);
return NextResponse.json(
{ success: false, error: '系统信息获取失败,请重试' },
{ status: 500 }
);
}
// 确保macAddress不是无效值
if (fingerprint.macAddress === '00:00:00:00:00:00') {
console.warn('[License] MAC地址无效,使用备用值');
fingerprint.macAddress = 'unknown';
}
// ✅ 安全改进:代理请求到许可证服务器(隐藏服务器地址)
const signedRequest = buildSignedLicenseRequest('register-free', {
fingerprint,
customerName,
contactInfo,
});
const response = await fetch(buildLicenseServerLicenseApiUrl('register-free'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedRequest),
});
if (!response.ok) {
let errorMessage = '激活失败,请检查网络连接';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
console.error('[License] 免费版注册失败:', sanitizeLicenseServerLogPayload(errorData));
} catch {
errorMessage = response.statusText || errorMessage;
console.error('[License] 免费版注册失败:', response.status, response.statusText);
}
return NextResponse.json(
{ success: false, error: errorMessage },
{ status: 400 }
);
}
const data = await response.json();
if (!data.success) {
console.error('[License] 免费版注册失败:', sanitizeLicenseServerLogPayload(data));
return NextResponse.json(
{ success: false, error: data.error || '激活失败,请检查网络连接' },
{ status: 400 }
);
}
const activationCode = data.data?.activationCode;
const signedLicense = data.license;
if (!activationCode || !signedLicense?.data || !signedLicense?.signature) {
console.error('[License] 免费版注册返回数据不完整:', sanitizeLicenseServerLogPayload(data));
return NextResponse.json(
{ success: false, error: '许可证服务返回数据异常,请稍后重试' },
{ status: 502 }
);
}
// ✅ 免费版也必须使用许可证服务端签名,避免保存不可验证的本地许可证
if (!verifyLicenseSignature(signedLicense.data, signedLicense.signature)) {
return NextResponse.json(
{ success: false, error: '许可证签名验证失败' },
{ status: 400 }
);
}
const bindingResult = validateSignedLicenseBinding(signedLicense, {
licenseKey: activationCode,
instanceId: fingerprint.instanceId,
});
if (!bindingResult.valid) {
return NextResponse.json(
{ success: false, error: bindingResult.error },
{ status: 400 }
);
}
console.log('[License] 免费版激活码:', maskSecret(activationCode), data.data.isExisting ? '(已存在)' : '(新注册)');
// ✅ 在后端保存许可证
await saveLicenseLocally(activationCode, signedLicense);
const moduleSyncResult = await syncPremiumModules();
if (!moduleSyncResult.synced && !moduleSyncResult.skipped) {
console.warn('[License] 授权模块清单同步失败:', moduleSyncResult.error);
}
// 初始化系统(创建默认管理员、部门、店铺等)
const initResult = await initializeSystem();
// 启动心跳
startHeartbeat();
const result = NextResponse.json({
success: true,
initialAdmin: initResult.admin?.created
? {
email: initResult.admin.email,
username: initResult.admin.username,
password: initResult.admin.passwordSource === 'generated' ? initResult.admin.initialPassword || null : null,
mustChangePassword: initResult.admin.mustChangePassword,
passwordSource: initResult.admin.passwordSource || null,
}
: null,
});
// 自动检测协议:根据请求的协议决定 cookie 的 secure 属性
const isHttps = request.headers.get('x-forwarded-proto') === 'https'
|| request.url.startsWith('https://');
// 设置许可证激活 cookie(用于 middleware 快速检查)
result.cookies.set('license_activated', 'true', {
httpOnly: true,
secure: isHttps,
sameSite: 'lax',
maxAge: 365 * 24 * 60 * 60, // 1年
path: '/',
});
return result;
} catch (error) {
console.error('免费试用激活失败:', error);
return NextResponse.json(
{ success: false, error: '服务器错误' },
{ status: 500 }
);
}
}