Files
kintone-vue-template/src/services/license-service.ts
2025-11-03 10:23:43 +08:00

320 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 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();
}
}