diff --git a/README.md b/README.md index 992fc00..602456f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# kintone-vue-ts-template +# kintone-vue-template 使用 Vue 、ts 和 Vite 创建 kintone plugin 的初始化模板,先由 [create-plugin](https://cybozu.dev/ja/kintone/sdk/development-environment/create-plugin/) 生成之后再手动引入 Vue。 diff --git a/src/components/LicenseStatus.vue b/src/components/LicenseStatus.vue index cc76e20..e01fbda 100644 --- a/src/components/LicenseStatus.vue +++ b/src/components/LicenseStatus.vue @@ -35,7 +35,7 @@ type LicenseDisplayInfo = { isPaid: boolean; expiryDate: string; isExpired: boolean; - remainingDays: number; + remainingDays?: number; }; // 状态管理 @@ -53,7 +53,6 @@ const licenseDisplayInfo = computed(() => { isPaid: false, expiryDate: $t('license.status.unknown'), isExpired: false, - remainingDays: 1, }; } @@ -110,7 +109,7 @@ const licenseStatusText = computed(() => { if (licenseDisplayInfo.value.isPaid) { return `${$t('license.status.permanentDisplay')}`; } - + // TODO let status = $t('license.expiry.expiryDate', { date: licenseDisplayInfo.value.expiryDate }); if (licenseDisplayInfo.value.isExpired) { @@ -119,8 +118,10 @@ const licenseStatusText = computed(() => { } const remainingDays = licenseDisplayInfo.value.remainingDays; - const days = $t('license.notification.days', remainingDays) - status += `(${days})`; + if (remainingDays !== undefined) { + const days = $t('license.notification.days', remainingDays) + status += `(${days})`; + } return status; }); diff --git a/src/services/license-service.ts b/src/services/license-service.ts index 0d83371..79cb244 100644 --- a/src/services/license-service.ts +++ b/src/services/license-service.ts @@ -7,14 +7,12 @@ 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 = ''; // ============ 基础工具函数 ============ @@ -49,20 +47,20 @@ export class LicenseService { static checkLicenseAvailable(savedLicense: SavedLicense): boolean { try { // 验证完整的JWT - const result = KJUR.jws.JWS.verifyJWT(savedLicense.jwt, rsaPublicKey.trim(), {alg: ['RS256']}); + const result = KJUR.jws.JWS.verifyJWT(savedLicense.jwt, rsaPublicKey.trim(), { alg: ['RS256'] }); if (!result) { - console.warn($t('license.error.jwtFailed', { e : '' })); + console.warn($t('license.error.jwtFailed', { e: '' })); return false; } } catch (error) { - console.warn($t('license.error.jwtFailed', { e : error })); + console.warn($t('license.error.jwtFailed', { e: error })); return false; } const license = savedLicense.licenseInfo; - const domain = this.getDomain() - const pluginId = this.getPluginId() + const domain = this.getDomain(); + const pluginId = this.getPluginId(); // 检查域名和插件ID是否与当前环境一致 if (license.domain !== domain || license.pluginId !== pluginId) { @@ -99,33 +97,56 @@ export class LicenseService { * 许可证验证 */ static async checkLicense(): Promise { - const localLicense = this.getLocalLicenseInfo() || undefined - if (localLicense) { - return { - isLicenseValid: true, - license: localLicense, - }; - } - return await this.checkLicenseRemote() + 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 { + const url = 'https://penguin-informed-mildly.ngrok-free.app/api/license/check'; + const method = 'POST'; + const headers = { 'Content-Type': 'application/json' }; + const body = { + domain, + pluginId, + pluginKey: 'kintone-vue-template', + }; + 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 { try { - // 这里应该是实际的API调用,暂时模拟创建加密的试用许可证 - const response = await this.mockRemoteCheck(this.getDomain(), this.getPluginId()); + const domain = this.getDomain(); + const pluginId = this.getPluginId(); - if (!response.success || !response.jwt) { + const jwt = await this.callRemoteLicenseAPI(domain, pluginId); + + if (!jwt) { return { isLicenseValid: false, isRemote: true, }; } - const jwt = response.jwt; - // 保存 JWT 到本地存储,获取保存的许可证结构 const savedLicense = LicenseStorage.saveLicense(jwt); if (!savedLicense) { @@ -143,7 +164,6 @@ export class LicenseService { license: savedLicense.licenseInfo, isRemote: true, }; - } catch (error) { console.error($t('license.error.fetchFailed', { e: error })); return { @@ -160,7 +180,7 @@ export class LicenseService { */ static isExpiringSoon(expiryTimestamp: number): boolean { const now = Date.now(); - const warningTime = expiryTimestamp - (this.WARNING_DAYS_BEFORE_EXPIRY * 24 * 60 * 60 * 1000); + const warningTime = expiryTimestamp - this.WARNING_DAYS_BEFORE_EXPIRY * 24 * 60 * 60 * 1000; return now >= warningTime && expiryTimestamp > now; } @@ -195,9 +215,9 @@ export class LicenseService { // 尚未到期 const remainingDays = this.getDaysRemaining(license!.expiredTime); if (remainingDays < 0) { - return + return; } - const days = $t('license.notification.days', remainingDays) + const days = $t('license.notification.days', remainingDays); message = $t('license.notification.warning', { plugin, days }); } else { // 既に期限切れ @@ -210,7 +230,7 @@ export class LicenseService { }); notification.open(); } else { - const link = `https://alicorn.cybozu.com/k/admin/app/${kintone.app.getId()}/plugin/config?pluginId=${this.getPluginId()}` + const link = `https://alicorn.cybozu.com/k/admin/app/${kintone.app.getId()}/plugin/config?pluginId=${this.getPluginId()}`; const notification = new Notification({ content: message + '
' + $t('license.notification.gotoLink', { link }), type: isWarning ? 'info' : 'danger', @@ -224,10 +244,7 @@ export class LicenseService { /** * 检查插件功能访问权限并加载插件(如果获得授权) */ - static async loadPluginIfAuthorized( - pluginId: string, - callback: () => void | Promise, - ) { + static async loadPluginIfAuthorized(pluginId: string, callback: () => void | Promise) { this.PLUGIN_ID = pluginId; try { // 检查许可证(内部已经包含本地检查无效时自动获取远程的逻辑) @@ -250,17 +267,18 @@ export class LicenseService { } // 许可证有效,如果快要到期,管理员可以看到警告 - if (isManager && - licenseCheck.license && - !licenseCheck.license.isPaid && - this.isExpiringSoon(licenseCheck.license.expiredTime)) { + 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')); } @@ -286,61 +304,4 @@ export class LicenseService { 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); - - return { - success: true, - jwt, - }; - } }