Compare commits
7 Commits
a4ae06ea52
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c55323d47d | |||
| 3198d6f646 | |||
| c4999c4804 | |||
| 63a2cfc186 | |||
| 393c637163 | |||
| fc3249987e | |||
| 308291825d |
93
README.md
93
README.md
@@ -1,8 +1,10 @@
|
|||||||
|
|
||||||
# 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。
|
使用 Vue 、ts 和 Vite 创建 kintone plugin 的初始化模板,先由 [create-plugin](https://cybozu.dev/ja/kintone/sdk/development-environment/create-plugin/) 生成之后再手动引入 Vue。
|
||||||
|
|
||||||
|
并且提供了 [License 检查](#license-检查) 的功能。
|
||||||
|
|
||||||
> プラグイン開発手順:https://cybozu.dev/ja/kintone/tips/development/plugins/development-plugin/
|
> プラグイン開発手順:https://cybozu.dev/ja/kintone/tips/development/plugins/development-plugin/
|
||||||
|
|
||||||
包括了以下 kintone 库:
|
包括了以下 kintone 库:
|
||||||
@@ -76,6 +78,38 @@ npm install # 或 yarn
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
3. 如果需要使用 License 检查,请参考: [License 检查](#license-检查)
|
||||||
|
|
||||||
|
需要修改 `src/config.json`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
{
|
||||||
|
"license": {
|
||||||
|
"enabled": true, // 是否开启 License 检查
|
||||||
|
"api": {
|
||||||
|
"checkUrl": "https://kintone.alicorns.co.jp/api/license/check", // server 路径,一般不需要修改
|
||||||
|
+ "pluginKey": 插件的 id,需要和后端数据库中保持一致
|
||||||
|
- "pluginKey": "kintone-vue-template"
|
||||||
|
},
|
||||||
|
"purchase": {
|
||||||
|
+ "url": 购买页面打开的链接
|
||||||
|
- "url": "https://alisurvey.alicorns.co.jp/s/Iuc5RxZv",
|
||||||
|
"formIds": {
|
||||||
|
+ "name": 表单中存储申请人的名字的 field id, 会设置为 kintone 的登陆用户名,为空就跳过
|
||||||
|
- "name": "input1761283314263",
|
||||||
|
+ "email": 表单中存储申请人 email 的 field id, 会设置为 kintone 的登陆邮箱,为空就跳过
|
||||||
|
- "email": "input1761283275767",
|
||||||
|
+ "domain": 表单中存储用户 url domain 的 field id, 会设置为 url
|
||||||
|
- "domain": "input1761283180784",
|
||||||
|
+ "pluginId": 表单中存储用户 plugin id 的 field id, 会设置为 plugin id
|
||||||
|
- "pluginId": "input1761283200616"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warningDaysBeforeExpiry": 7 // 进行提醒的剩余天数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
# 编译流程
|
# 编译流程
|
||||||
|
|
||||||
@@ -161,6 +195,8 @@ npm run build-upload # 或 yarn build-upload
|
|||||||
├── package.json
|
├── package.json
|
||||||
├── private.ppk # 当前 plugin 密钥,首次 build 自动生成
|
├── private.ppk # 当前 plugin 密钥,首次 build 自动生成
|
||||||
├── README.md
|
├── README.md
|
||||||
|
├── rsa_private.pem # license 验证密钥
|
||||||
|
├── rsa_public.pem # license 公钥,需要被添加到密钥检查的后端系统中
|
||||||
├── tsconfig.json
|
├── tsconfig.json
|
||||||
└── vite.config.ts # 主要的 vite 配置
|
└── vite.config.ts # 主要的 vite 配置
|
||||||
└── vite.iife.config.ts # 用于打包 desktop/mobile 文件的配置
|
└── vite.iife.config.ts # 用于打包 desktop/mobile 文件的配置
|
||||||
@@ -229,7 +265,6 @@ client.app...
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 关于 i18n
|
## 关于 i18n
|
||||||
|
|
||||||
在 desktop/mobile:
|
在 desktop/mobile:
|
||||||
@@ -439,3 +474,57 @@ kuc 没有实现插槽,所以应该当成一个普通组件使用:
|
|||||||
const buyButton = shallowRef<HTMLButtonElement | null>(null);
|
const buyButton = shallowRef<HTMLButtonElement | null>(null);
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License 检查
|
||||||
|
|
||||||
|
目前 License 的检查使用的是后端传一个 RSA 密钥加密的 JWT,前端使用公钥验证是否被修改过,并且保存在 Localstorage 中。
|
||||||
|
|
||||||
|
每隔一天都会从后端重新获取新的 License 信息。
|
||||||
|
|
||||||
|
默认试用是 30 天,还剩 7 天的时候会进行提醒。(在 `license-service.ts` 中修改)
|
||||||
|
|
||||||
|
当点击购买的时候,会跳转到一个外部网站,这里可以使用 AliSurvey 进行表单填写。
|
||||||
|
|
||||||
|
|
||||||
|
### 创建密钥
|
||||||
|
|
||||||
|
创建密钥可以使用 openssl 命令行工具:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
openssl genrsa -out ./rsa_private.pem 2048
|
||||||
|
```
|
||||||
|
|
||||||
|
然后公钥可以放在同一个文件夹中:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
openssl rsa -in ./rsa_private.pem -pubout -out ./rsa_public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
> 公钥需要被放到后端系统中
|
||||||
|
|
||||||
|
|
||||||
|
### 使用
|
||||||
|
|
||||||
|
在 desktop/mobile 中只要在所有的逻辑外部多包裹一层 `LicenseService.loadPluginIfAuthorized()` :
|
||||||
|
|
||||||
|
```ts
|
||||||
|
kintone.events.on('app.record.index.show', async () => {
|
||||||
|
LicenseService.loadPluginIfAuthorized(PLUGIN_ID, // <--- 内部会进行 license 判断
|
||||||
|
async () => {
|
||||||
|
// 已有的逻辑代码
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
在 vue 中只要在顶部引入 ` LicenseStatus.vue`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 许可证状态信息 -->
|
||||||
|
<LicenseStatus />
|
||||||
|
|
||||||
|
<h2 class="settings-heading">{{ $t('config.title') }}</h2>
|
||||||
|
```
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"build": "vite build && npm run pkg",
|
"build": "vite build && npm run pkg",
|
||||||
"build:prod": "cross-env BUILD_MODE=production vite build && npm run pkg",
|
"build:prod": "cross-env BUILD_MODE=production vite build && npm run pkg",
|
||||||
"build-upload": "npm run build && npm run upload",
|
"build-upload": "npm run build && npm run upload",
|
||||||
|
"build-upload:prod": "npm run build:prod && npm run upload",
|
||||||
"pkg": "kintone-plugin-packer --ppk private.ppk --out dist/plugin.zip dist/src",
|
"pkg": "kintone-plugin-packer --ppk private.ppk --out dist/plugin.zip dist/src",
|
||||||
"upload": "kintone-plugin-uploader --base-url https://alicorn.cybozu.com --username maxz --password 7ld7i8vd dist/plugin.zip "
|
"upload": "kintone-plugin-uploader --base-url https://alicorn.cybozu.com --username maxz --password 7ld7i8vd dist/plugin.zip "
|
||||||
},
|
},
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
"@kintone/rest-api-client": "^5.7.5",
|
"@kintone/rest-api-client": "^5.7.5",
|
||||||
"jsrsasign": "^11.1.0",
|
"jsrsasign": "^11.1.0",
|
||||||
"jsrsasign-util": "^1.0.5",
|
"jsrsasign-util": "^1.0.5",
|
||||||
"kintone-ui-component": "1.22.0",
|
"kintone-ui-component": "1.23.0",
|
||||||
"rollup-plugin-css-only": "^4.5.2",
|
"rollup-plugin-css-only": "^4.5.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^11.0.1"
|
"vue-i18n": "^11.0.1"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import { LicenseService } from '@/services/license-service';
|
import { LicenseService } from '@/services/license-service';
|
||||||
import type { LicenseInfo } from '@/types/license';
|
import type { LicenseInfo } from '@/types/license';
|
||||||
import { LicenseStorage } from '@/utils/license-storage';
|
import { LicenseStorage } from '@/utils/license-storage';
|
||||||
|
import config from '@/config.json';
|
||||||
|
|
||||||
const { t: $t } = useI18n();// 配置国际化
|
const { t: $t } = useI18n();// 配置国际化
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ type LicenseDisplayInfo = {
|
|||||||
isPaid: boolean;
|
isPaid: boolean;
|
||||||
expiryDate: string;
|
expiryDate: string;
|
||||||
isExpired: boolean;
|
isExpired: boolean;
|
||||||
remainingDays: number;
|
remainingDays?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
@@ -53,7 +54,6 @@ const licenseDisplayInfo = computed<LicenseDisplayInfo>(() => {
|
|||||||
isPaid: false,
|
isPaid: false,
|
||||||
expiryDate: $t('license.status.unknown'),
|
expiryDate: $t('license.status.unknown'),
|
||||||
isExpired: false,
|
isExpired: false,
|
||||||
remainingDays: 1,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +66,13 @@ const licenseDisplayInfo = computed<LicenseDisplayInfo>(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
if (!config.license.enabled) {
|
||||||
|
// 许可证功能未启用,不显示组件
|
||||||
|
shown.value = false;
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await LicenseService.checkLicense();
|
const result = await LicenseService.checkLicense();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
licenseInfo.value = result.license;
|
licenseInfo.value = result.license;
|
||||||
@@ -110,7 +117,7 @@ const licenseStatusText = computed(() => {
|
|||||||
if (licenseDisplayInfo.value.isPaid) {
|
if (licenseDisplayInfo.value.isPaid) {
|
||||||
return `<span class="text-green">${$t('license.status.permanentDisplay')}</span>`;
|
return `<span class="text-green">${$t('license.status.permanentDisplay')}</span>`;
|
||||||
}
|
}
|
||||||
|
// TODO
|
||||||
let status = $t('license.expiry.expiryDate', { date: licenseDisplayInfo.value.expiryDate });
|
let status = $t('license.expiry.expiryDate', { date: licenseDisplayInfo.value.expiryDate });
|
||||||
|
|
||||||
if (licenseDisplayInfo.value.isExpired) {
|
if (licenseDisplayInfo.value.isExpired) {
|
||||||
@@ -119,8 +126,10 @@ const licenseStatusText = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const remainingDays = licenseDisplayInfo.value.remainingDays;
|
const remainingDays = licenseDisplayInfo.value.remainingDays;
|
||||||
|
if (remainingDays !== undefined) {
|
||||||
const days = $t('license.notification.days', remainingDays)
|
const days = $t('license.notification.days', remainingDays)
|
||||||
status += `(${days})`;
|
status += `(${days})`;
|
||||||
|
}
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
});
|
});
|
||||||
@@ -141,7 +150,20 @@ function purchaseLicense() {
|
|||||||
const { name, email } = kintone.getLoginUser();
|
const { name, email } = kintone.getLoginUser();
|
||||||
const domain = licenseInfo.value?.domain;
|
const domain = licenseInfo.value?.domain;
|
||||||
const pluginId = licenseInfo.value?.pluginId;
|
const pluginId = licenseInfo.value?.pluginId;
|
||||||
window.open(`https://alisurvey.alicorns.co.jp/s/Iuc5RxZv?input1761283180784=${domain}&input1761283200616=${pluginId}&input1761283314263=${name}&input1761283275767=${email}`);
|
|
||||||
|
const params = {
|
||||||
|
name: { inputId: config.license.purchase.formIds.name, value: name },
|
||||||
|
email: { inputId: config.license.purchase.formIds.email, value: email },
|
||||||
|
domain: { inputId: config.license.purchase.formIds.domain, value: domain },
|
||||||
|
pluginId: { inputId: config.license.purchase.formIds.pluginId, value: pluginId }
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryParams = Object.values(params)
|
||||||
|
.filter(param => param.value)
|
||||||
|
.map(param => `${param.inputId}=${encodeURIComponent(param.value as string)}`)
|
||||||
|
.join('&');
|
||||||
|
|
||||||
|
window.open(`${config.license.purchase.url}?${queryParams}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hidePaidMsg() {
|
function hidePaidMsg() {
|
||||||
|
|||||||
19
src/config.json
Normal file
19
src/config.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"license": {
|
||||||
|
"enabled": true,
|
||||||
|
"api": {
|
||||||
|
"checkUrl": "https://kintone.alicorns.co.jp/api/license/check",
|
||||||
|
"pluginKey": "kintone-vue-template"
|
||||||
|
},
|
||||||
|
"purchase": {
|
||||||
|
"url": "https://alisurvey.alicorns.co.jp/s/Iuc5RxZv",
|
||||||
|
"formIds": {
|
||||||
|
"name": "input1761283314263",
|
||||||
|
"email": "input1761283275767",
|
||||||
|
"domain": "input1761283180784",
|
||||||
|
"pluginId": "input1761283200616"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warningDaysBeforeExpiry": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,7 @@ import { Button } from 'kintone-ui-component/lib/button';
|
|||||||
(function (PLUGIN_ID) {
|
(function (PLUGIN_ID) {
|
||||||
kintone.events.on('app.record.index.show', async () => {
|
kintone.events.on('app.record.index.show', async () => {
|
||||||
// 授权了才能使用
|
// 授权了才能使用
|
||||||
LicenseService.loadPluginIfAuthorized(PLUGIN_ID,
|
LicenseService.loadPluginIfAuthorized(PLUGIN_ID, async () => {
|
||||||
async () => {
|
|
||||||
// 获取当前应用ID
|
// 获取当前应用ID
|
||||||
const appIdNum = kintone.app.getId();
|
const appIdNum = kintone.app.getId();
|
||||||
if (!appIdNum) {
|
if (!appIdNum) {
|
||||||
@@ -55,7 +54,6 @@ import { Button } from 'kintone-ui-component/lib/button';
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
headerSpace.appendChild(button);
|
headerSpace.appendChild(button);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
})(kintone.$PLUGIN_ID);
|
})(kintone.$PLUGIN_ID);
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { MobileButton } from 'kintone-ui-component/lib/mobile/button';
|
|||||||
(function (PLUGIN_ID) {
|
(function (PLUGIN_ID) {
|
||||||
kintone.events.on('mobile.app.record.index.show', () => {
|
kintone.events.on('mobile.app.record.index.show', () => {
|
||||||
// 授权了才能使用
|
// 授权了才能使用
|
||||||
LicenseService.loadPluginIfAuthorized(PLUGIN_ID,
|
LicenseService.loadPluginIfAuthorized(PLUGIN_ID, async () => {
|
||||||
async () => {
|
|
||||||
// 获取当前应用ID
|
// 获取当前应用ID
|
||||||
const appIdNum = kintone.mobile.app.getId();
|
const appIdNum = kintone.mobile.app.getId();
|
||||||
if (!appIdNum) {
|
if (!appIdNum) {
|
||||||
@@ -55,7 +54,6 @@ import { MobileButton } from 'kintone-ui-component/lib/mobile/button';
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
headerSpace.appendChild(button);
|
headerSpace.appendChild(button);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
})(kintone.$PLUGIN_ID);
|
})(kintone.$PLUGIN_ID);
|
||||||
@@ -7,14 +7,12 @@ import { MobileNotification } from 'kintone-ui-component/lib/mobile/notification
|
|||||||
import manifestJson from '@/manifest.json';
|
import manifestJson from '@/manifest.json';
|
||||||
import { KJUR } from 'jsrsasign';
|
import { KJUR } from 'jsrsasign';
|
||||||
import rsaPublicKey from '../../rsa_public.pem?raw';
|
import rsaPublicKey from '../../rsa_public.pem?raw';
|
||||||
import rsaPrivateKey from '../../rsa_private.pem?raw';
|
import config from '@/config.json';
|
||||||
|
|
||||||
const { t: $t } = i18n.global;
|
const { t: $t } = i18n.global;
|
||||||
|
|
||||||
export class LicenseService {
|
export class LicenseService {
|
||||||
// 常量定义
|
// 常量定义
|
||||||
private static readonly WARNING_DAYS_BEFORE_EXPIRY = 7;
|
|
||||||
private static readonly TRIAL_DATE = 30;
|
|
||||||
private static PLUGIN_ID: string = '';
|
private static PLUGIN_ID: string = '';
|
||||||
// ============ 基础工具函数 ============
|
// ============ 基础工具函数 ============
|
||||||
|
|
||||||
@@ -49,20 +47,20 @@ export class LicenseService {
|
|||||||
static checkLicenseAvailable(savedLicense: SavedLicense): boolean {
|
static checkLicenseAvailable(savedLicense: SavedLicense): boolean {
|
||||||
try {
|
try {
|
||||||
// 验证完整的JWT
|
// 验证完整的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) {
|
if (!result) {
|
||||||
console.warn($t('license.error.jwtFailed', { e : '' }));
|
console.warn($t('license.error.jwtFailed', { e: '' }));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn($t('license.error.jwtFailed', { e : error }));
|
console.warn($t('license.error.jwtFailed', { e: error }));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const license = savedLicense.licenseInfo;
|
const license = savedLicense.licenseInfo;
|
||||||
|
|
||||||
const domain = this.getDomain()
|
const domain = this.getDomain();
|
||||||
const pluginId = this.getPluginId()
|
const pluginId = this.getPluginId();
|
||||||
|
|
||||||
// 检查域名和插件ID是否与当前环境一致
|
// 检查域名和插件ID是否与当前环境一致
|
||||||
if (license.domain !== domain || license.pluginId !== pluginId) {
|
if (license.domain !== domain || license.pluginId !== pluginId) {
|
||||||
@@ -99,33 +97,62 @@ export class LicenseService {
|
|||||||
* 许可证验证
|
* 许可证验证
|
||||||
*/
|
*/
|
||||||
static async checkLicense(): Promise<LicenseCheckResult> {
|
static async checkLicense(): Promise<LicenseCheckResult> {
|
||||||
const localLicense = this.getLocalLicenseInfo() || undefined
|
if (!config.license.enabled || rsaPublicKey.trim() === '') {
|
||||||
|
return {
|
||||||
|
isLicenseValid: true,
|
||||||
|
license: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const localLicense = this.getLocalLicenseInfo() || undefined;
|
||||||
if (localLicense) {
|
if (localLicense) {
|
||||||
return {
|
return {
|
||||||
isLicenseValid: true,
|
isLicenseValid: true,
|
||||||
license: localLicense,
|
license: localLicense,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return await this.checkLicenseRemote()
|
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> {
|
static async checkLicenseRemote(): Promise<LicenseCheckResult> {
|
||||||
try {
|
try {
|
||||||
// 这里应该是实际的API调用,暂时模拟创建加密的试用许可证
|
const domain = this.getDomain();
|
||||||
const response = await this.mockRemoteCheck(this.getDomain(), this.getPluginId());
|
const pluginId = this.getPluginId();
|
||||||
|
|
||||||
if (!response.success || !response.jwt) {
|
const jwt = await this.callRemoteLicenseAPI(domain, pluginId);
|
||||||
|
|
||||||
|
if (!jwt) {
|
||||||
return {
|
return {
|
||||||
isLicenseValid: false,
|
isLicenseValid: false,
|
||||||
isRemote: true,
|
isRemote: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const jwt = response.jwt;
|
|
||||||
|
|
||||||
// 保存 JWT 到本地存储,获取保存的许可证结构
|
// 保存 JWT 到本地存储,获取保存的许可证结构
|
||||||
const savedLicense = LicenseStorage.saveLicense(jwt);
|
const savedLicense = LicenseStorage.saveLicense(jwt);
|
||||||
if (!savedLicense) {
|
if (!savedLicense) {
|
||||||
@@ -143,7 +170,6 @@ export class LicenseService {
|
|||||||
license: savedLicense.licenseInfo,
|
license: savedLicense.licenseInfo,
|
||||||
isRemote: true,
|
isRemote: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error($t('license.error.fetchFailed', { e: error }));
|
console.error($t('license.error.fetchFailed', { e: error }));
|
||||||
return {
|
return {
|
||||||
@@ -160,7 +186,7 @@ export class LicenseService {
|
|||||||
*/
|
*/
|
||||||
static isExpiringSoon(expiryTimestamp: number): boolean {
|
static isExpiringSoon(expiryTimestamp: number): boolean {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const warningTime = expiryTimestamp - (this.WARNING_DAYS_BEFORE_EXPIRY * 24 * 60 * 60 * 1000);
|
const warningTime = expiryTimestamp - config.license.warningDaysBeforeExpiry * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
return now >= warningTime && expiryTimestamp > now;
|
return now >= warningTime && expiryTimestamp > now;
|
||||||
}
|
}
|
||||||
@@ -195,9 +221,9 @@ export class LicenseService {
|
|||||||
// 尚未到期
|
// 尚未到期
|
||||||
const remainingDays = this.getDaysRemaining(license!.expiredTime);
|
const remainingDays = this.getDaysRemaining(license!.expiredTime);
|
||||||
if (remainingDays < 0) {
|
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 });
|
message = $t('license.notification.warning', { plugin, days });
|
||||||
} else {
|
} else {
|
||||||
// 既に期限切れ
|
// 既に期限切れ
|
||||||
@@ -210,7 +236,7 @@ export class LicenseService {
|
|||||||
});
|
});
|
||||||
notification.open();
|
notification.open();
|
||||||
} else {
|
} 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({
|
const notification = new Notification({
|
||||||
content: message + '<br />' + $t('license.notification.gotoLink', { link }),
|
content: message + '<br />' + $t('license.notification.gotoLink', { link }),
|
||||||
type: isWarning ? 'info' : 'danger',
|
type: isWarning ? 'info' : 'danger',
|
||||||
@@ -224,12 +250,15 @@ export class LicenseService {
|
|||||||
/**
|
/**
|
||||||
* 检查插件功能访问权限并加载插件(如果获得授权)
|
* 检查插件功能访问权限并加载插件(如果获得授权)
|
||||||
*/
|
*/
|
||||||
static async loadPluginIfAuthorized(
|
static async loadPluginIfAuthorized(pluginId: string, callback: () => void | Promise<void>) {
|
||||||
pluginId: string,
|
|
||||||
callback: () => void | Promise<void>,
|
|
||||||
) {
|
|
||||||
this.PLUGIN_ID = pluginId;
|
this.PLUGIN_ID = pluginId;
|
||||||
try {
|
try {
|
||||||
|
if (!config.license.enabled) {
|
||||||
|
// 许可证功能未启用,直接加载插件
|
||||||
|
await callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查许可证(内部已经包含本地检查无效时自动获取远程的逻辑)
|
// 检查许可证(内部已经包含本地检查无效时自动获取远程的逻辑)
|
||||||
const licenseCheck = await this.checkLicense();
|
const licenseCheck = await this.checkLicense();
|
||||||
|
|
||||||
@@ -250,17 +279,18 @@ export class LicenseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 许可证有效,如果快要到期,管理员可以看到警告
|
// 许可证有效,如果快要到期,管理员可以看到警告
|
||||||
if (isManager &&
|
if (
|
||||||
|
isManager &&
|
||||||
licenseCheck.license &&
|
licenseCheck.license &&
|
||||||
!licenseCheck.license.isPaid &&
|
!licenseCheck.license.isPaid &&
|
||||||
this.isExpiringSoon(licenseCheck.license.expiredTime)) {
|
this.isExpiringSoon(licenseCheck.license.expiredTime)
|
||||||
|
) {
|
||||||
// 管理员可以看到过期弹框
|
// 管理员可以看到过期弹框
|
||||||
this.showNotification(licenseCheck.license, true);
|
this.showNotification(licenseCheck.license, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 许可证有效,可以加载插件功能
|
// 许可证有效,可以加载插件功能
|
||||||
await callback();
|
await callback();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn($t('license.error.checkFailed'));
|
console.warn($t('license.error.checkFailed'));
|
||||||
}
|
}
|
||||||
@@ -286,61 +316,4 @@ export class LicenseService {
|
|||||||
LicenseStorage.clearLicense(this.getPluginId());
|
LicenseStorage.clearLicense(this.getPluginId());
|
||||||
return await this.checkLicenseRemote();
|
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