This commit is contained in:
2025-10-23 23:52:26 +08:00
parent 3961285b32
commit e86afd8487
8 changed files with 188 additions and 50 deletions

View File

@@ -0,0 +1,354 @@
import type { LicenseInfo, LicenseCheckResult } from '@/types/license';
import i18n from '@/i18n';
import { LicenseStorage } from '@/utils/license-storage';
import { PermissionService } from '@/utils/permissions';
import { Notification } from 'kintone-ui-component/lib/notification';
import { MobileNotification } from 'kintone-ui-component/lib/mobile/notification';
import manifestJson from '@/manifest.json';
import { KJUR } from 'jsrsasign';
import rsaPublicKey from '../../rsa_public.pem?raw';
import rsaPrivateKey from '../../rsa_private.pem?raw';
const { t: $t } = i18n.global;
export class LicenseService {
// 常量定义
private static readonly WARNING_DAYS_BEFORE_EXPIRY = 7;
private static readonly TRIAL_DATE = 30;
private static PLUGIN_ID: string = '';
// ============ 基础工具函数 ============
/**
* 获取域名
*/
static getDomain(): string {
return window.location.hostname;
}
/**
* 获取插件名
*/
static getPluginName(): string {
const pluginName = manifestJson.name as Record<string, string>;
const userLang = kintone.getLoginUser().language;
const plugin = pluginName[userLang] || pluginName.ja;
return plugin;
}
/**
* 获取插件ID
*/
static getPluginId(): string {
return this.PLUGIN_ID || new URLSearchParams(window.location.search).get('pluginId') || '';
}
/**
* 检查许可证是否有效
* 参数是解码后的JWT结构验证签名和payload进行原有检查
*/
static checkLicenseAvailable(jwtString: string, decodedJWT: KJUR.jws.JWS.JWSResult): boolean {
try {
// 验证完整的JWT
const result = KJUR.jws.JWS.verifyJWT(jwtString, rsaPublicKey.trim(), {alg: ['RS256']});
if (!result) {
console.warn('JWT signature verification failed');
return false;
}
} catch (error) {
console.error('JWT verification or parsing failed:', error);
return false;
}
// JWT验证通过从payloadPP解析license进行原有检查
const license: LicenseInfo = JSON.parse(decodedJWT.payloadPP);
const domain = this.getDomain()
const pluginId = this.getPluginId()
// 检查域名和插件ID是否与当前环境一致
if (license.domain !== domain || license.pluginId !== pluginId) {
return false;
}
// 检查是否付费
if (license.isPaid) {
return true;
}
// 检查存储是否过期(是否同一天)
if (!this.isToday(license.fetchTime)) {
// 不是同一天,已过期
return false;
}
// 检查试用是否到期
const expiredTime = new Date(license.expiredTime);
if (expiredTime < new Date()) {
return false;
}
return true;
}
static isToday(timestamp: number): boolean {
const fetchDate = new Date(timestamp).toDateString();
const todayStr = new Date().toDateString();
return fetchDate == todayStr;
}
/**
* 许可证验证
*/
static async checkLicense(): Promise<LicenseCheckResult> {
const localLicense = this.getLocalLicenseInfo() || undefined
if (localLicense) {
return {
isLicenseValid: true,
license: localLicense,
};
}
return await this.checkLicenseRemote()
}
/**
* 远程许可证验证(模拟)
*/
static async checkLicenseRemote(): Promise<LicenseCheckResult> {
try {
// 这里应该是实际的API调用暂时模拟创建加密的试用许可证
const response = await this.mockRemoteCheck(this.getDomain(), this.getPluginId());
if (!response.success || !response.jwt) {
return {
isLicenseValid: false,
isRemote: true,
};
}
const jwt = response.jwt;
// 保存 JWT 到本地存储,获取解码后的结构
const decodedJWT = LicenseStorage.saveLicense(jwt);
// 从解码后的JWT提取许可证信息
const license: LicenseInfo = JSON.parse(decodedJWT.payloadPP);
// 使用解码后的JWT进行验证
const isValid = this.checkLicenseAvailable(jwt, decodedJWT);
return {
isLicenseValid: isValid,
license,
isRemote: true,
};
} catch (error) {
console.error($t('license.error.fetchFailed', { e: error }));
return {
isLicenseValid: false,
isRemote: true,
};
}
}
// ============ 数据处理函数 ============
/**
* 检查是否快要到期
*/
static isExpiringSoon(expiryTimestamp: number): boolean {
const now = Date.now();
const warningTime = expiryTimestamp - (this.WARNING_DAYS_BEFORE_EXPIRY * 24 * 60 * 60 * 1000);
return now >= warningTime && expiryTimestamp > now;
}
/**
* 格式化到期时间
*/
static formatExpiryDate(timestamp: number): string {
if (timestamp === -1) return $t('license.status.permanent');
return new Date(timestamp).toLocaleDateString(kintone.getLoginUser().language);
}
/**
* 计算剩余天数
*/
static getDaysRemaining(timestamp: number): number {
if (timestamp === -1) return Infinity;
const now = new Date().getTime();
const remainingMs = timestamp - now;
return Math.floor(remainingMs / (24 * 60 * 60 * 1000));
}
// ============ UI显示函数 ============
/**
* 显示到期警告通知
*/
static async showNotification(license: LicenseInfo | undefined, isWarning: boolean) {
let message;
const plugin = this.getPluginName();
if (isWarning) {
// 尚未到期
const remainingDays = this.getDaysRemaining(license!.expiredTime);
if (remainingDays < 0) {
return
}
const days = $t('license.notification.days', remainingDays)
message = $t('license.notification.warning', { plugin, days });
} else {
// 既に期限切れ
message = $t('license.notification.expired', { plugin });
}
if (await kintone.isMobilePage()) {
const notification = new MobileNotification({
content: message,
});
notification.open();
} else {
const link = `https://alicorn.cybozu.com/k/admin/app/${kintone.app.getId()}/plugin/config?pluginId=${this.getPluginId()}`
const notification = new Notification({
content: message + '<br />' + $t('license.notification.gotoLink', { link }),
type: isWarning ? 'info' : 'danger',
});
notification.open();
}
}
// ============ 主要入口函数 ============
/**
* 检查插件功能访问权限并加载插件(如果获得授权)
*/
static async loadPluginIfAuthorized(
pluginId: string,
callback: () => void | Promise<void>,
) {
this.PLUGIN_ID = pluginId;
try {
// 检查许可证(内部已经包含本地检查无效时自动获取远程的逻辑)
const licenseCheck = await this.checkLicense();
// 检查权限
const permissions = await PermissionService.checkPermissions();
const isManager = permissions.canManagePlugins;
// 许可证无效的情况,直接返回不加载
if (!licenseCheck.isLicenseValid) {
if (!isManager) {
// 普通用户静默输出
console.warn($t('license.status.expiredDisplay'));
} else {
// 管理员可以看到过期弹框
this.showNotification(licenseCheck.license, false);
}
return;
}
// 许可证有效,如果快要到期,管理员可以看到警告
if (isManager &&
licenseCheck.license &&
!licenseCheck.license.isPaid &&
this.isExpiringSoon(licenseCheck.license.expiredTime)) {
// 管理员可以看到过期弹框
this.showNotification(licenseCheck.license, true);
}
// 许可证有效,可以加载插件功能
await callback();
} catch (error) {
console.warn($t('license.error.checkFailed'));
}
}
// ============ 许可证信息管理 ============
/**
* 获取许可证信息(用于显示)
*/
static getLocalLicenseInfo(): LicenseInfo | null {
const decodedJWT = LicenseStorage.getLicense(this.getPluginId());
if (!decodedJWT) return null;
try {
return JSON.parse(decodedJWT.payloadPP) as LicenseInfo;
} catch (error) {
console.error('Failed to parse payloadPP from decoded JWT:', error);
return null;
}
}
/**
* 强制刷新许可证(清除本地缓存重新检查)
*/
static async forceRefreshLicense(): Promise<LicenseCheckResult> {
// 清除本地缓存
LicenseStorage.clearLicense(this.getPluginId());
return await this.checkLicenseRemote();
}
// ============ 模拟/测试函数 ============
/**
* 创建试用许可证
*/
private static mockCreateTrialLicense(): LicenseInfo {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + this.TRIAL_DATE);
expiryDate.setHours(23, 59, 59, 999);
return {
expiredTime: expiryDate.getTime(),
isPaid: false,
domain: this.getDomain(),
pluginId: this.getPluginId(),
fetchTime: new Date().getTime(),
version: 1,
};
}
/**
* 生成JWT token内部方法
*/
private static generateJWT(licenseInfo: LicenseInfo): string {
const header = {
alg: 'RS256',
typ: 'JWT'
};
const payload = {
...licenseInfo,
iat: Math.floor(Date.now() / 1000)
};
const sHeader = JSON.stringify(header);
const sPayload = JSON.stringify(payload);
const jwt = KJUR.jws.JWS.sign(null, sHeader, sPayload, rsaPrivateKey.trim());
return jwt;
}
/**
* 模拟远程验证生产环境中会被真实API替换
*/
private static async mockRemoteCheck(domain: string, pluginId: string): Promise<{ success: boolean; jwt?: string }> {
// 模拟API调用这里总是返回加密的试用许可证
// 生产环境这里会调用后端API: POST /api/license/check
const license = this.mockCreateTrialLicense();
const jwt = this.generateJWT(license);
// 模拟有时创建失败的情况1%概率)
if (Math.random() < 0.01) {
return { success: false };
}
return {
success: true,
jwt,
};
}
}