320 lines
9.3 KiB
TypeScript
320 lines
9.3 KiB
TypeScript
import type { LicenseInfo, LicenseCheckResult, SavedLicense } 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 config from '@/config.json';
|
||
|
||
const { t: $t } = i18n.global;
|
||
|
||
export class LicenseService {
|
||
// 常量定义
|
||
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(savedLicense: SavedLicense): boolean {
|
||
try {
|
||
// 验证完整的JWT
|
||
const result = KJUR.jws.JWS.verifyJWT(savedLicense.jwt, rsaPublicKey.trim(), { alg: ['RS256'] });
|
||
if (!result) {
|
||
console.warn($t('license.error.jwtFailed', { e: '' }));
|
||
return false;
|
||
}
|
||
} catch (error) {
|
||
console.warn($t('license.error.jwtFailed', { e: error }));
|
||
return false;
|
||
}
|
||
|
||
const license = savedLicense.licenseInfo;
|
||
|
||
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> {
|
||
if (!config.license.enabled || rsaPublicKey.trim() === '') {
|
||
return {
|
||
isLicenseValid: true,
|
||
license: undefined,
|
||
};
|
||
}
|
||
const localLicense = this.getLocalLicenseInfo() || undefined;
|
||
if (localLicense) {
|
||
return {
|
||
isLicenseValid: true,
|
||
license: localLicense,
|
||
};
|
||
}
|
||
return await this.checkLicenseRemote();
|
||
}
|
||
|
||
/**
|
||
* 调用远程许可证API
|
||
*/
|
||
private static async callRemoteLicenseAPI(domain: string, pluginId: string): Promise<string | null> {
|
||
const url = config.license.api.checkUrl;
|
||
const method = 'POST';
|
||
const headers = { 'Content-Type': 'application/json' };
|
||
const body = {
|
||
domain,
|
||
pluginId,
|
||
pluginKey: config.license.api.pluginKey,
|
||
};
|
||
const proxyResponse = await kintone.proxy(url, method, headers, body);
|
||
if (proxyResponse[1] !== 200) {
|
||
throw new Error(`API request failed with status: ${proxyResponse[1]}`);
|
||
}
|
||
const response = JSON.parse(proxyResponse[0]);
|
||
if (!response || !response.success || !response.jwt) {
|
||
return null;
|
||
}
|
||
return response.jwt;
|
||
}
|
||
|
||
/**
|
||
* 远程许可证验证
|
||
*/
|
||
static async checkLicenseRemote(): Promise<LicenseCheckResult> {
|
||
try {
|
||
const domain = this.getDomain();
|
||
const pluginId = this.getPluginId();
|
||
|
||
const jwt = await this.callRemoteLicenseAPI(domain, pluginId);
|
||
|
||
if (!jwt) {
|
||
return {
|
||
isLicenseValid: false,
|
||
isRemote: true,
|
||
};
|
||
}
|
||
|
||
// 保存 JWT 到本地存储,获取保存的许可证结构
|
||
const savedLicense = LicenseStorage.saveLicense(jwt);
|
||
if (!savedLicense) {
|
||
return {
|
||
isLicenseValid: false,
|
||
isRemote: true,
|
||
};
|
||
}
|
||
|
||
// 使用解析后的JWT进行验证
|
||
const isValid = this.checkLicenseAvailable(savedLicense);
|
||
|
||
return {
|
||
isLicenseValid: isValid,
|
||
license: savedLicense.licenseInfo,
|
||
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 - config.license.warningDaysBeforeExpiry * 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 {
|
||
if (!config.license.enabled) {
|
||
// 许可证功能未启用,直接加载插件
|
||
await callback();
|
||
return;
|
||
}
|
||
|
||
// 检查许可证(内部已经包含本地检查无效时自动获取远程的逻辑)
|
||
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 savedLicense = LicenseStorage.getLicense(this.getPluginId());
|
||
if (!savedLicense) return null;
|
||
|
||
return savedLicense.licenseInfo;
|
||
}
|
||
|
||
/**
|
||
* 强制刷新许可证(清除本地缓存重新检查)
|
||
*/
|
||
static async forceRefreshLicense(): Promise<LicenseCheckResult> {
|
||
// 清除本地缓存
|
||
LicenseStorage.clearLicense(this.getPluginId());
|
||
return await this.checkLicenseRemote();
|
||
}
|
||
}
|