flnxi-Wu/lib/license/license-service.ts
2026-05-25 14:52:24 +08:00

481 lines
13 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 { initializeSystem as initializeSystemCore } from '../system-init';
import type {
LicenseConfig,
SignedLicense,
ActivateResponse,
VerifyResponse,
HeartbeatResponse,
FeatureKey,
LocalLicenseData,
} from './types';
import { LICENSE_CONFIGS } from './types';
// 宽限期(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;
function isLicenseConfigExpired(license: LicenseConfig | null): boolean {
return Boolean(license?.expiresAt && new Date(license.expiresAt) < new Date());
}
function extractLicenseConfig(license: SignedLicense): LicenseConfig {
const { issuedAt: _issuedAt, ...config } = license.data;
return config;
}
function normalizeLocalLicenseData(localData: LocalLicenseData): LocalLicenseData | null {
if (
!localData ||
typeof localData.licenseKey !== 'string' ||
typeof localData.licenseData !== 'string' ||
typeof localData.lastVerified !== 'string'
) {
return null;
}
try {
const decrypted = decryptLocal(localData.licenseData);
const signedLicense = JSON.parse(decrypted) as SignedLicense;
if (!signedLicense?.data || !signedLicense?.signature) {
return null;
}
if (!verifyLicenseSignature(signedLicense.data, signedLicense.signature)) {
return null;
}
return {
...localData,
cachedConfig: extractLicenseConfig(signedLicense),
};
} catch (error) {
console.warn('[License] 本地许可证校验失败:', error);
return null;
}
}
async function getCurrentLicenseConfig(): Promise<LicenseConfig | null> {
const localLicense = await getLocalLicense();
const currentConfig = localLicense?.cachedConfig ?? null;
if (!localLicense || !currentConfig || isLicenseConfigExpired(currentConfig)) {
cachedLicense = null;
lastVerified = localLicense?.lastVerified ? new Date(localLicense.lastVerified) : null;
return null;
}
cachedLicense = currentConfig;
lastVerified = new Date(localLicense.lastVerified);
return currentConfig;
}
/**
* 激活许可证
*
* ⚠️ 注意:此函数现在只用于客户端调用素材系统自己的API
* 实际的激活逻辑已移至 app/api/license/activate/route.ts(后端代理)
*
* 安全改进:
* - 客户端不再直接访问许可证服务器
* - 硬件指纹在后端生成(更安全)
* - 许可证服务器地址完全隐藏
*/
export async function activate(licenseKey: string): Promise<{ success: boolean; error?: string }> {
try {
// ✅ 安全改进:调用素材系统自己的API(不再直接调用许可证服务器)
const response = await fetch('/api/license/activate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
licenseKey,
}),
});
const data = await response.json();
if (!data.success) {
return { success: false, error: data.error || '激活失败' };
}
// 重新加载许可证(因为后端已保存)
await loadLicenseFromLocal();
// 更新缓存
if (cachedLicense) {
lastVerified = new Date();
}
return { success: true };
} catch (error) {
console.error('激活失败:', error);
return { success: false, error: '网络错误,请检查网络连接' };
}
}
/**
* 激活免费试用版
*
* ⚠️ 注意:此函数现在只用于客户端调用素材系统自己的API
* 实际的注册逻辑已移至 app/api/license/activate-free/route.ts(后端代理)
*
* 安全改进:
* - 客户端不再直接访问许可证服务器
* - 硬件指纹在后端生成(更安全)
* - 许可证服务器地址完全隐藏
*
* @param customerName 客户名称(用于许可证系统后台管理)
* @param contactInfo 联系方式(手机/微信/邮箱)
*/
export async function activateFreeTrial(customerName: string, contactInfo: string = ''): Promise<{ success: boolean; error?: string }> {
try {
// ✅ 安全改进:调用素材系统自己的API(不再直接调用许可证服务器)
const response = await fetch('/api/license/activate-free', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customerName,
contactInfo,
}),
});
const data = await response.json();
if (!data.success) {
return { success: false, error: data.error || '激活失败,请检查网络连接' };
}
// 重新加载许可证(因为后端已保存)
await loadLicenseFromLocal();
// 更新缓存
if (cachedLicense) {
lastVerified = new Date();
}
return { success: true };
} catch (error) {
console.error('免费试用激活失败:', error);
return { success: false, error: '网络错误,请检查网络连接后重试' };
}
}
/**
* 初始化系统(创建默认管理员、部门、店铺等)
*/
export async function initializeSystem() {
return initializeSystemCore();
}
/**
* 验证许可证
*
* ⚠️ 注意:此函数现在只用于客户端调用素材系统自己的API
* 实际的验证逻辑已移至 app/api/license/verify/route.ts(后端代理)
*
* 安全改进:
* - 客户端不再直接访问许可证服务器
* - 硬件指纹在后端生成(更安全)
* - 许可证服务器地址完全隐藏
*/
export async function verify(): Promise<{ valid: boolean; error?: string; revoked?: boolean }> {
try {
// 先检查本地缓存
const localLicense = await getLocalLicense();
if (!localLicense) {
return { valid: false, error: '未找到许可证' };
}
// ✅ 安全改进:调用素材系统自己的API(不再直接调用许可证服务器)
const response = await fetch('/api/license/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
if (!data.valid) {
// 检查是否是被吊销
if (data.revoked) {
console.log('[License] 许可证已被吊销:', data.error);
cachedLicense = null;
return { valid: false, error: data.error || '许可证已被吊销', revoked: true };
}
// 其他错误,检查宽限期
if (isWithinGracePeriod(localLicense.lastVerified)) {
cachedLicense = localLicense.cachedConfig;
return { valid: true };
}
return { valid: false, error: data.error || '验证失败' };
}
// 重新加载许可证(因为后端已更新)
await loadLicenseFromLocal();
// 更新内存缓存
if (cachedLicense) {
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: '网络错误' };
}
}
/**
* 发送心跳
*
* ⚠️ 注意:此函数现在只用于客户端调用素材系统自己的API
* 实际的心跳逻辑已移至 app/api/license/heartbeat/route.ts(后端代理)
*
* 安全改进:
* - 客户端不再直接访问许可证服务器
* - 硬件指纹在后端生成(更安全)
* - 许可证服务器地址完全隐藏
*
* @returns 如果许可证被吊销返回 true
*/
export async function heartbeat(): Promise<boolean> {
try {
const localLicense = await getLocalLicense();
if (!localLicense) {
return false;
}
// ✅ 安全改进:调用素材系统自己的API(不再直接调用许可证服务器)
const response = await fetch('/api/license/heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
// 检查是否被吊销
if (data.revoked) {
console.log('[License] 心跳检测到许可证已被吊销');
cachedLicense = null;
return true; // 返回 true 表示被吊销
}
if (data.acknowledged) {
// 重新加载许可证(因为后端已更新)
await loadLicenseFromLocal();
lastVerified = new Date();
}
return false;
} catch (error) {
console.error('心跳发送失败:', error);
return false;
}
}
/**
* 获取当前许可证配置
*/
export function getLicense(): LicenseConfig | null {
if (isLicenseConfigExpired(cachedLicense)) {
cachedLicense = null;
return null;
}
return cachedLicense;
}
/**
* 检查功能是否可用
*/
export async function isFeatureEnabled(feature: FeatureKey): Promise<boolean> {
const currentLicense = await getCurrentLicenseConfig();
return Boolean(currentLicense?.features[feature]);
}
/**
* 检查是否达到限制
*/
export async function checkLimit(type: 'users' | 'materials'): Promise<{ allowed: boolean; current: number; max: number }> {
const currentLicense = await getCurrentLicenseConfig();
if (!currentLicense) {
return { allowed: false, current: 0, max: 0 };
}
const max = type === 'users' ? currentLicense.limits.maxUsers : currentLicense.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 async function getLicenseType(): Promise<string> {
const currentLicense = await getCurrentLicenseConfig();
return currentLicense?.type || 'FREE';
}
/**
* 检查许可证是否已过期
*/
export function isExpired(): boolean {
return isLicenseConfigExpired(cachedLicense);
}
/**
* 启动心跳定时器
*/
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();
}
}
// ============ 内部辅助函数 ============
export async function saveLicenseLocally(licenseKey: string, license: SignedLicense): Promise<void> {
const instanceId = await getInstanceId();
const currentConfig = extractLicenseConfig(license);
const localData: LocalLicenseData = {
instanceId,
licenseKey,
licenseData: encryptLocal(JSON.stringify(license)),
lastVerified: new Date().toISOString(),
cachedConfig: currentConfig,
};
await prisma.systemConfig.upsert({
where: { key: 'license' },
update: { value: JSON.stringify(localData) },
create: { key: 'license', value: JSON.stringify(localData) },
});
cachedLicense = isLicenseConfigExpired(currentConfig) ? null : currentConfig;
lastVerified = new Date(localData.lastVerified);
}
export async function updateLocalLicense(license: SignedLicense): Promise<void> {
const existing = await getLocalLicense();
if (!existing) return;
const currentConfig = extractLicenseConfig(license);
const localData: LocalLicenseData = {
...existing,
licenseData: encryptLocal(JSON.stringify(license)),
lastVerified: new Date().toISOString(),
cachedConfig: currentConfig,
};
await prisma.systemConfig.update({
where: { key: 'license' },
data: { value: JSON.stringify(localData) },
});
cachedLicense = isLicenseConfigExpired(currentConfig) ? null : currentConfig;
lastVerified = new Date(localData.lastVerified);
}
/**
* 从本地加载许可证并更新内存缓存
*/
async function loadLicenseFromLocal(): Promise<void> {
const localLicense = await getLocalLicense();
if (localLicense) {
cachedLicense = localLicense.cachedConfig;
lastVerified = new Date(localLicense.lastVerified);
} else {
cachedLicense = null;
lastVerified = null;
}
}
export async function getLocalLicense(): Promise<LocalLicenseData | null> {
try {
const config = await prisma.systemConfig.findUnique({
where: { key: 'license' },
});
if (!config) return null;
const parsed = JSON.parse(config.value) as LocalLicenseData;
return normalizeLocalLicenseData(parsed);
} catch {
return null;
}
}
/**
* 清除本地许可证缓存(许可证被吊销时调用)
*/
export async function clearLocalLicense(): Promise<void> {
try {
await prisma.systemConfig.delete({
where: { key: 'license' },
});
console.log('[License] 本地许可证缓存已清除');
} catch (error) {
console.error('[License] 清除本地许可证缓存失败:', error);
}
}
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;
}