fix using server api
This commit is contained in:
@@ -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。
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ type LicenseDisplayInfo = {
|
||||
isPaid: boolean;
|
||||
expiryDate: string;
|
||||
isExpired: boolean;
|
||||
remainingDays: number;
|
||||
remainingDays?: number;
|
||||
};
|
||||
|
||||
// 状态管理
|
||||
@@ -53,7 +53,6 @@ const licenseDisplayInfo = computed<LicenseDisplayInfo>(() => {
|
||||
isPaid: false,
|
||||
expiryDate: $t('license.status.unknown'),
|
||||
isExpired: false,
|
||||
remainingDays: 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,7 +109,7 @@ const licenseStatusText = computed(() => {
|
||||
if (licenseDisplayInfo.value.isPaid) {
|
||||
return `<span class="text-green">${$t('license.status.permanentDisplay')}</span>`;
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
@@ -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<LicenseCheckResult> {
|
||||
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<string | null> {
|
||||
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<LicenseCheckResult> {
|
||||
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 + '<br />' + $t('license.notification.gotoLink', { link }),
|
||||
type: isWarning ? 'info' : 'danger',
|
||||
@@ -224,10 +244,7 @@ export class LicenseService {
|
||||
/**
|
||||
* 检查插件功能访问权限并加载插件(如果获得授权)
|
||||
*/
|
||||
static async loadPluginIfAuthorized(
|
||||
pluginId: string,
|
||||
callback: () => void | Promise<void>,
|
||||
) {
|
||||
static async loadPluginIfAuthorized(pluginId: string, callback: () => void | Promise<void>) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user