- 新增许可证服务器 (license-server/) - 新增 Agent 部署模块 (flnxi-agent/) - 添加许可证验证和激活页面 - 添加 Harbor 镜像同步脚本 - 更新素材系统支持许可证验证
371 lines
9.3 KiB
TypeScript
371 lines
9.3 KiB
TypeScript
import { prisma } from '@/lib/prisma';
|
||
import { generateFingerprint, getInstanceId } from './fingerprint-service';
|
||
import { verifyLicenseSignature, encryptLocal, decryptLocal } from './crypto-service';
|
||
import type {
|
||
LicenseConfig,
|
||
SignedLicense,
|
||
ActivateResponse,
|
||
VerifyResponse,
|
||
HeartbeatResponse,
|
||
FeatureKey,
|
||
LocalLicenseData,
|
||
} from './types';
|
||
import { LICENSE_CONFIGS } from './types';
|
||
|
||
// 许可证服务器地址
|
||
const LICENSE_SERVER_URL = process.env.LICENSE_SERVER_URL || 'http://45.249.208.118:3001';
|
||
|
||
// 宽限期(7天)
|
||
const GRACE_PERIOD_DAYS = 7;
|
||
|
||
// 心跳间隔(1小时)
|
||
const HEARTBEAT_INTERVAL = 60 * 60 * 1000;
|
||
|
||
// 缓存的许可证配置
|
||
let cachedLicense: LicenseConfig | null = null;
|
||
let lastVerified: Date | null = null;
|
||
let heartbeatTimer: NodeJS.Timeout | null = null;
|
||
|
||
/**
|
||
* 激活许可证
|
||
*/
|
||
export async function activate(licenseKey: string): Promise<{ success: boolean; error?: string }> {
|
||
try {
|
||
const fingerprint = await generateFingerprint();
|
||
|
||
const response = await fetch(`${LICENSE_SERVER_URL}/api/license/activate`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
licenseKey,
|
||
fingerprint,
|
||
timestamp: Date.now(),
|
||
}),
|
||
});
|
||
|
||
const data: ActivateResponse = await response.json();
|
||
|
||
if (!data.success || !data.license) {
|
||
return { success: false, error: data.error || '激活失败' };
|
||
}
|
||
|
||
// 验证签名
|
||
if (!verifyLicenseSignature(data.license.data, data.license.signature)) {
|
||
return { success: false, error: '许可证签名验证失败' };
|
||
}
|
||
|
||
// 保存到本地
|
||
await saveLicenseLocally(licenseKey, data.license);
|
||
|
||
// 更新缓存
|
||
cachedLicense = data.license.data;
|
||
lastVerified = new Date();
|
||
|
||
// 启动心跳
|
||
startHeartbeat();
|
||
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('激活失败:', error);
|
||
return { success: false, error: '网络错误,请检查网络连接' };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 激活免费试用版
|
||
*/
|
||
export async function activateFreeTrial(): Promise<{ success: boolean; error?: string }> {
|
||
try {
|
||
const instanceId = await getInstanceId();
|
||
|
||
// 创建免费版配置
|
||
const freeConfig: LicenseConfig = {
|
||
...LICENSE_CONFIGS.FREE,
|
||
expiresAt: null,
|
||
};
|
||
|
||
// 保存到本地(无需服务器验证)
|
||
const localData: LocalLicenseData = {
|
||
instanceId,
|
||
licenseKey: 'FREE-TRIAL',
|
||
licenseData: encryptLocal(JSON.stringify(freeConfig)),
|
||
lastVerified: new Date().toISOString(),
|
||
cachedConfig: freeConfig,
|
||
};
|
||
|
||
await prisma.systemConfig.upsert({
|
||
where: { key: 'license' },
|
||
update: { value: JSON.stringify(localData) },
|
||
create: { key: 'license', value: JSON.stringify(localData) },
|
||
});
|
||
|
||
// 更新缓存
|
||
cachedLicense = freeConfig;
|
||
lastVerified = new Date();
|
||
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('免费试用激活失败:', error);
|
||
return { success: false, error: '激活失败' };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证许可证
|
||
*/
|
||
export async function verify(): Promise<{ valid: boolean; error?: string }> {
|
||
try {
|
||
// 先检查本地缓存
|
||
const localLicense = await getLocalLicense();
|
||
|
||
if (!localLicense) {
|
||
return { valid: false, error: '未找到许可证' };
|
||
}
|
||
|
||
// 免费试用版不需要在线验证
|
||
if (localLicense.licenseKey === 'FREE-TRIAL') {
|
||
cachedLicense = localLicense.cachedConfig;
|
||
lastVerified = new Date();
|
||
return { valid: true };
|
||
}
|
||
|
||
const fingerprint = await generateFingerprint();
|
||
|
||
const response = await fetch(`${LICENSE_SERVER_URL}/api/license/verify`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
instanceId: fingerprint.instanceId,
|
||
fingerprint,
|
||
timestamp: Date.now(),
|
||
}),
|
||
});
|
||
|
||
const data: VerifyResponse = await response.json();
|
||
|
||
if (!data.valid || !data.license) {
|
||
// 检查宽限期
|
||
if (isWithinGracePeriod(localLicense.lastVerified)) {
|
||
cachedLicense = localLicense.cachedConfig;
|
||
return { valid: true };
|
||
}
|
||
return { valid: false, error: data.error || '验证失败' };
|
||
}
|
||
|
||
// 验证签名
|
||
if (!verifyLicenseSignature(data.license.data, data.license.signature)) {
|
||
return { valid: false, error: '许可证签名验证失败' };
|
||
}
|
||
|
||
// 更新本地缓存
|
||
await updateLocalLicense(data.license);
|
||
|
||
// 更新内存缓存
|
||
cachedLicense = data.license.data;
|
||
lastVerified = new Date();
|
||
|
||
return { valid: true };
|
||
} catch (error) {
|
||
console.error('验证失败:', error);
|
||
|
||
// 网络错误时检查宽限期
|
||
const localLicense = await getLocalLicense();
|
||
if (localLicense && isWithinGracePeriod(localLicense.lastVerified)) {
|
||
cachedLicense = localLicense.cachedConfig;
|
||
return { valid: true };
|
||
}
|
||
|
||
return { valid: false, error: '网络错误' };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送心跳
|
||
*/
|
||
export async function heartbeat(): Promise<void> {
|
||
try {
|
||
const localLicense = await getLocalLicense();
|
||
if (!localLicense || localLicense.licenseKey === 'FREE-TRIAL') {
|
||
return;
|
||
}
|
||
|
||
const fingerprint = await generateFingerprint();
|
||
|
||
// 获取使用统计
|
||
const [userCount, materialCount] = await Promise.all([
|
||
prisma.user.count(),
|
||
prisma.material.count(),
|
||
]);
|
||
|
||
const response = await fetch(`${LICENSE_SERVER_URL}/api/license/heartbeat`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
instanceId: fingerprint.instanceId,
|
||
fingerprint,
|
||
usage: { userCount, materialCount },
|
||
timestamp: Date.now(),
|
||
}),
|
||
});
|
||
|
||
const data: HeartbeatResponse = await response.json();
|
||
|
||
if (data.acknowledged && data.license) {
|
||
// 更新本地缓存
|
||
await updateLocalLicense(data.license);
|
||
cachedLicense = data.license.data;
|
||
}
|
||
|
||
lastVerified = new Date();
|
||
} catch (error) {
|
||
console.error('心跳发送失败:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前许可证配置
|
||
*/
|
||
export function getLicense(): LicenseConfig | null {
|
||
return cachedLicense;
|
||
}
|
||
|
||
/**
|
||
* 检查功能是否可用
|
||
*/
|
||
export function isFeatureEnabled(feature: FeatureKey): boolean {
|
||
if (!cachedLicense) {
|
||
return false;
|
||
}
|
||
return cachedLicense.features[feature];
|
||
}
|
||
|
||
/**
|
||
* 检查是否达到限制
|
||
*/
|
||
export async function checkLimit(type: 'users' | 'materials'): Promise<{ allowed: boolean; current: number; max: number }> {
|
||
if (!cachedLicense) {
|
||
return { allowed: false, current: 0, max: 0 };
|
||
}
|
||
|
||
const max = type === 'users' ? cachedLicense.limits.maxUsers : cachedLicense.limits.maxMaterials;
|
||
|
||
// -1 表示无限制
|
||
if (max === -1) {
|
||
return { allowed: true, current: 0, max: -1 };
|
||
}
|
||
|
||
const current = type === 'users'
|
||
? await prisma.user.count()
|
||
: await prisma.material.count();
|
||
|
||
return {
|
||
allowed: current < max,
|
||
current,
|
||
max,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取许可证类型
|
||
*/
|
||
export function getLicenseType(): string {
|
||
return cachedLicense?.type || 'FREE';
|
||
}
|
||
|
||
/**
|
||
* 检查许可证是否已过期
|
||
*/
|
||
export function isExpired(): boolean {
|
||
if (!cachedLicense || !cachedLicense.expiresAt) {
|
||
return false;
|
||
}
|
||
return new Date(cachedLicense.expiresAt) < new Date();
|
||
}
|
||
|
||
/**
|
||
* 启动心跳定时器
|
||
*/
|
||
export function startHeartbeat(): void {
|
||
if (heartbeatTimer) {
|
||
clearInterval(heartbeatTimer);
|
||
}
|
||
heartbeatTimer = setInterval(heartbeat, HEARTBEAT_INTERVAL);
|
||
}
|
||
|
||
/**
|
||
* 停止心跳定时器
|
||
*/
|
||
export function stopHeartbeat(): void {
|
||
if (heartbeatTimer) {
|
||
clearInterval(heartbeatTimer);
|
||
heartbeatTimer = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化许可证服务
|
||
*/
|
||
export async function initLicenseService(): Promise<void> {
|
||
const result = await verify();
|
||
if (result.valid) {
|
||
startHeartbeat();
|
||
}
|
||
}
|
||
|
||
// ============ 内部辅助函数 ============
|
||
|
||
async function saveLicenseLocally(licenseKey: string, license: SignedLicense): Promise<void> {
|
||
const instanceId = await getInstanceId();
|
||
|
||
const localData: LocalLicenseData = {
|
||
instanceId,
|
||
licenseKey,
|
||
licenseData: encryptLocal(JSON.stringify(license)),
|
||
lastVerified: new Date().toISOString(),
|
||
cachedConfig: license.data,
|
||
};
|
||
|
||
await prisma.systemConfig.upsert({
|
||
where: { key: 'license' },
|
||
update: { value: JSON.stringify(localData) },
|
||
create: { key: 'license', value: JSON.stringify(localData) },
|
||
});
|
||
}
|
||
|
||
async function updateLocalLicense(license: SignedLicense): Promise<void> {
|
||
const existing = await getLocalLicense();
|
||
if (!existing) return;
|
||
|
||
const localData: LocalLicenseData = {
|
||
...existing,
|
||
licenseData: encryptLocal(JSON.stringify(license)),
|
||
lastVerified: new Date().toISOString(),
|
||
cachedConfig: license.data,
|
||
};
|
||
|
||
await prisma.systemConfig.update({
|
||
where: { key: 'license' },
|
||
data: { value: JSON.stringify(localData) },
|
||
});
|
||
}
|
||
|
||
async function getLocalLicense(): Promise<LocalLicenseData | null> {
|
||
try {
|
||
const config = await prisma.systemConfig.findUnique({
|
||
where: { key: 'license' },
|
||
});
|
||
|
||
if (!config) return null;
|
||
|
||
return JSON.parse(config.value) as LocalLicenseData;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function isWithinGracePeriod(lastVerifiedStr: string): boolean {
|
||
const lastVerified = new Date(lastVerifiedStr);
|
||
const gracePeriodEnd = new Date(lastVerified.getTime() + GRACE_PERIOD_DAYS * 24 * 60 * 60 * 1000);
|
||
return new Date() < gracePeriodEnd;
|
||
}
|