flnxi-Wu/lib/license/license-service.ts
WuYongJie ba98bf22cc feat: 添加许可证系统、Agent部署模块和Harbor镜像管理
- 新增许可证服务器 (license-server/)
- 新增 Agent 部署模块 (flnxi-agent/)
- 添加许可证验证和激活页面
- 添加 Harbor 镜像同步脚本
- 更新素材系统支持许可证验证
2025-12-15 21:19:46 +08:00

371 lines
9.3 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 { 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;
}