apply license
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -11,6 +11,7 @@ declare module 'vue' {
|
||||
CellInput: typeof import('./src/components/basic/conditions/CellInput.vue')['default']
|
||||
Config: typeof import('./src/components/Config.vue')['default']
|
||||
ErrorDialog: typeof import('./src/components/basic/ErrorDialog.vue')['default']
|
||||
LicenseStatus: typeof import('./src/components/LicenseStatus.vue')['default']
|
||||
PluginDropdown: typeof import('./src/components/basic/PluginDropdown.vue')['default']
|
||||
PluginInput: typeof import('./src/components/basic/PluginInput.vue')['default']
|
||||
PluginLabel: typeof import('./src/components/basic/PluginLabel.vue')['default']
|
||||
|
||||
70
package-lock.json
generated
70
package-lock.json
generated
@@ -9,7 +9,9 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@kintone/rest-api-client": "^5.7.5",
|
||||
"kintone-ui-component": "1.18.0",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"jsrsasign-util": "^1.0.5",
|
||||
"kintone-ui-component": "1.21.0",
|
||||
"rollup-plugin-css-only": "^4.5.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.0.1"
|
||||
@@ -19,6 +21,7 @@
|
||||
"@kintone/dts-gen": "^8.1.1",
|
||||
"@kintone/plugin-packer": "^8.1.3",
|
||||
"@kintone/plugin-uploader": "^9.1.2",
|
||||
"@types/jsrsasign": "^10.5.15",
|
||||
"@types/node-rsa": "^1.1.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
@@ -2649,6 +2652,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsrsasign": {
|
||||
"version": "10.5.15",
|
||||
"resolved": "https://registry.npmmirror.com/@types/jsrsasign/-/jsrsasign-10.5.15.tgz",
|
||||
"integrity": "sha512-3stUTaSRtN09PPzVWR6aySD9gNnuymz+WviNHoTb85dKu+BjaV4uBbWWGykBBJkfwPtcNZVfTn2lbX00U+yhpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
@@ -3450,12 +3460,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@webcomponents/webcomponentsjs": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz",
|
||||
"integrity": "sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
|
||||
@@ -7339,6 +7343,12 @@
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
||||
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
@@ -7349,6 +7359,24 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsrsasign": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/jsrsasign/-/jsrsasign-11.1.0.tgz",
|
||||
"integrity": "sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/kjur/jsrsasign#donations"
|
||||
}
|
||||
},
|
||||
"node_modules/jsrsasign-util": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/jsrsasign-util/-/jsrsasign-util-1.0.5.tgz",
|
||||
"integrity": "sha512-e5Kp8aaT5GH2c5X8j4uaJruYmT4GcnaGb47nw8m60YqPywtnOtTISZ9hZgtZ3a+jh7B27bU2LCf3Y32wZyfhtQ==",
|
||||
"dependencies": {
|
||||
"jsonc-parser": ">= 0.0.1",
|
||||
"jsrsasign": ">= 4.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
@@ -7386,16 +7414,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/kintone-ui-component": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmmirror.com/kintone-ui-component/-/kintone-ui-component-1.18.0.tgz",
|
||||
"integrity": "sha512-cwNhOMAfZ5TBzNe4edGwQ3LeEJ466d+GwSWVQ0k43zVYkWCBkFzU68s/zzjJUKxFtjmIkND+77Lr6IolpBqRcw==",
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmmirror.com/kintone-ui-component/-/kintone-ui-component-1.21.0.tgz",
|
||||
"integrity": "sha512-GxxRw24v3p//ao5/KKyGnlh3vpkYmmyqm9NduYx1dCivRZMifweI5VhImhLw/QkAVHUOHowK3HVWIM5efOw0jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"core-js": "^3.38.1",
|
||||
"lit": "^3.2.0",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"uuid": "^10.0.0"
|
||||
"core-js": "^3.42.0",
|
||||
"lit": "^3.3.0",
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/language-subtag-registry": {
|
||||
@@ -8863,12 +8889,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||
@@ -10547,16 +10567,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
|
||||
@@ -7,13 +7,16 @@
|
||||
"vite:build": "vite build",
|
||||
"build": "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",
|
||||
"upload": "kintone-plugin-uploader --base-url https://alicorn.cybozu.com --username maxz --password 7ld7i8vd dist/plugin.zip "
|
||||
},
|
||||
"dependencies": {
|
||||
"@kintone/rest-api-client": "^5.7.5",
|
||||
"kintone-ui-component": "1.18.0",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"jsrsasign-util": "^1.0.5",
|
||||
"kintone-ui-component": "1.21.0",
|
||||
"rollup-plugin-css-only": "^4.5.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.0.1"
|
||||
@@ -23,6 +26,7 @@
|
||||
"@kintone/dts-gen": "^8.1.1",
|
||||
"@kintone/plugin-packer": "^8.1.3",
|
||||
"@kintone/plugin-uploader": "^9.1.2",
|
||||
"@types/jsrsasign": "^10.5.15",
|
||||
"@types/node-rsa": "^1.1.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
|
||||
28
rsa_private.pem
Normal file
28
rsa_private.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDAOp+f4vMaBPla
|
||||
Y59TTqIhDS7OKlXtJo3vUssv3rYnAWw/dIRQqJF7quddxHeMykr3PMiZ0YES16Ps
|
||||
t1U/vrL/koOPB2w7nRTg/GruVR5BULe/VV/fehBqsn/eC91RbYKNWjLlkHhWn/u/
|
||||
U0xaZkJCKsgpfMFeLRu7BBwOi9mQ4a27YOhgb0dcf553EzKSbkL1po4ZZCJ0GJt5
|
||||
Wozklz4wI+2wqqvykeK+THUAOC1FSx6wmFWDNG7l8hSe/li8Ju78+6kXz0NjvIfx
|
||||
uBfkAYnI1UPAefrhOIWc+2hc2G8jJX+Xe2iCY9VRliAlBYXQxD2mmMNNhTTlj/sz
|
||||
zwloAlkpAgMBAAECggEADxXqsK+tArCQ6GTjZt8dE86cNZDBT+GpMjbDUbKUL/PJ
|
||||
d3dg6jrjDptRxJw7rOsJPrrgm/WtlU7NWLE7OaFGcePL0EiLAr39RU954tH/lFej
|
||||
JpsAHE0mMawSzdhiL4GSb8kRhSOKgMiAXLpK8S3k6vAUPPFDSZfs/QcXCUCKEU0J
|
||||
24wU12pnVgwuTRsb3lkov4ke52FwmANESdTHIT99mF5WR0FLCK2UwVKd2FT0lu/e
|
||||
G/bx+2s3iaWOMhVC8eypvXjB8v+d7cIX1SWTdua29dwMfjaxK4srnzG6/xJUlhAV
|
||||
MP3Xu6c9pazxfoxPkYCCCAX695a7wsTTRILzG818mwKBgQDlL86HDFpO5ZOAkRfT
|
||||
wnKszOqphQ4iNjIDOD7VfBUXzcaf8tsocq9uDXW1F5eYxOs5CMuDepatC+KJ5tR2
|
||||
1o9wZ0dAWJbBQEtvGP9WM+k9+GVHre+BlaeeszwR7H9Z/fz796lu1cqvtEhXdmzE
|
||||
MDAPX5o78/KimP4OvjKcHzqg/wKBgQDWt+yV/6S1JYXri5IMVnbsdXoCvA5Cmixu
|
||||
LFURNX3V7AleJ3y5TJpApdH0Yda7DJFBOXrYEku7Bdl+iuVEN7kb8Oio9/PQgbsQ
|
||||
7+E8CzDAMTc7blMfKoxjn0qsM846XYtv6wc5HuQGxD1kKYq0rbUQS3TxgfzsO9VG
|
||||
gpdpBjfd1wKBgBMUd80mah5HXpBFhAZNGd7o8GBMg3C1slQySojbW6Yvq2mFfllP
|
||||
susmk7YP0L8XJb4JzWeUvRaK9sEkRmveUPK6pmPk/Cf6gk0td5RlnfVayJO/F0Fi
|
||||
hCHiKg4T8kY7ruQLKj/f7I/UInGlmkh6+oVNIDn9hSWkNwJjMzNsJiVhAoGASHs/
|
||||
3wa0J/4prfCodYe+j2W8sS2gNrVqqCpI8Q04lD0gkGsN/FMygv422KMqZGwCoJzx
|
||||
rfzHGbQIzmG+sP+KejchouBIqqsQZdBEHQu+AjLa7TH51zx6tapw/55hUGyBhF83
|
||||
Sf32hZ47BxPK4eD9jSmbqWby5R/xh+LPj8FsnHMCgYEApp7hIVSX3FIe1M1oGVbB
|
||||
cLzSb6wirKU6RcVF7rJ8IAh7IouuOluZ2SdcM6Pc8Jm1VXLi0pnj1SMwKaMrt98t
|
||||
j6I1YKGWCaoXmQBs/CSUmJU2KUk2L0N/Go7xxb0BD9IhZVyd+9ge4moUTYApTNi9
|
||||
cOTdlhJogUO+mvk98/r/Hc0=
|
||||
-----END PRIVATE KEY-----
|
||||
9
rsa_public.pem
Normal file
9
rsa_public.pem
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwDqfn+LzGgT5WmOfU06i
|
||||
IQ0uzipV7SaN71LLL962JwFsP3SEUKiRe6rnXcR3jMpK9zzImdGBEtej7LdVP76y
|
||||
/5KDjwdsO50U4Pxq7lUeQVC3v1Vf33oQarJ/3gvdUW2CjVoy5ZB4Vp/7v1NMWmZC
|
||||
QirIKXzBXi0buwQcDovZkOGtu2DoYG9HXH+edxMykm5C9aaOGWQidBibeVqM5Jc+
|
||||
MCPtsKqr8pHivkx1ADgtRUsesJhVgzRu5fIUnv5YvCbu/PupF89DY7yH8bgX5AGJ
|
||||
yNVDwHn64TiFnPtoXNhvIyV/l3togmPVUZYgJQWF0MQ9ppjDTYU05Y/7M88JaAJZ
|
||||
KQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -1,4 +1,7 @@
|
||||
<template>
|
||||
<!-- 许可证状态信息 -->
|
||||
<LicenseStatus />
|
||||
|
||||
<h2 class="settings-heading">{{ $t('config.title') }}</h2>
|
||||
<p class="kintoneplugin-desc">{{ $t('config.desc') }}</p>
|
||||
|
||||
@@ -30,6 +33,7 @@ import {
|
||||
import { isType, type OneOf, type Properties } from '@/js/kintone-rest-api-client';
|
||||
import type { CachedData, FieldsInfo, JoinTable, SavedData } from '@/types/model';
|
||||
import type { Spinner } from 'kintone-ui-component';
|
||||
import LicenseStatus from './LicenseStatus.vue';
|
||||
|
||||
import { onMounted, watch, provide, reactive, ref, shallowRef, nextTick } from 'vue';
|
||||
|
||||
|
||||
180
src/components/LicenseStatus.vue
Normal file
180
src/components/LicenseStatus.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div v-if="shown" class="license-status-info">
|
||||
<div class="license-content">
|
||||
<div class="license-status-text">
|
||||
<strong>{{ $t('license.status.label') }}</strong>
|
||||
<span v-if="licenseDisplayInfo" v-html="licenseStatusText"></span><span v-else> ... </span>
|
||||
</div>
|
||||
<div class="license-actions">
|
||||
<template v-if="!licenseDisplayInfo.isPaid">
|
||||
<button class="action-btn" @click="refreshLicenseStatus" :disabled="checking" ref="checkButton">{{ $t('license.button.checkLicense.label') }}</button>
|
||||
<kuc-tooltip :title="$t('license.button.checkLicense.desc')" :container="checkButton"></kuc-tooltip>
|
||||
<button class="action-btn main" @click="purchaseLicense" ref="buyButton">{{ $t('license.button.purchase.label') }}
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="open-icon">
|
||||
<path d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<kuc-tooltip :title="$t('license.button.purchase.desc')" :container="buyButton"></kuc-tooltip>
|
||||
</template>
|
||||
<button v-else class="action-btn" @click="hidePaidMsg">{{ $t('license.button.hide') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, shallowRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { LicenseService } from '@/services/license-service';
|
||||
import type { LicenseInfo } from '@/types/license';
|
||||
import { LicenseStorage } from '@/utils/license-storage';
|
||||
import config from '@/config.json';
|
||||
|
||||
const { t: $t } = useI18n();// 配置国际化
|
||||
|
||||
type LicenseDisplayInfo = {
|
||||
isPaid: boolean;
|
||||
expiryDate: string;
|
||||
isExpired: boolean;
|
||||
remainingDays?: number;
|
||||
};
|
||||
|
||||
// 状态管理
|
||||
const licenseInfo = ref<LicenseInfo | undefined>();
|
||||
const loading = ref(true);
|
||||
const shown = ref(false);
|
||||
const checking = ref(false);
|
||||
const checkButton = shallowRef<HTMLButtonElement | null>(null);
|
||||
const buyButton = shallowRef<HTMLButtonElement | null>(null);
|
||||
|
||||
// 许可证显示信息
|
||||
const licenseDisplayInfo = computed<LicenseDisplayInfo>(() => {
|
||||
if (!licenseInfo.value || loading.value) {
|
||||
return {
|
||||
isPaid: false,
|
||||
expiryDate: $t('license.status.unknown'),
|
||||
isExpired: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isPaid: licenseInfo.value.isPaid,
|
||||
expiryDate: LicenseService.formatExpiryDate(licenseInfo.value.expiredTime),
|
||||
isExpired: licenseInfo.value.expiredTime < Date.now() && !licenseInfo.value.isPaid,
|
||||
remainingDays: LicenseService.getDaysRemaining(licenseInfo.value.expiredTime),
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!config.license.enabled) {
|
||||
// 许可证功能未启用,不显示组件
|
||||
shown.value = false;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await LicenseService.checkLicense();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
licenseInfo.value = result.license;
|
||||
|
||||
shown.value = isPaidAreaShown();
|
||||
if (!shown.value) {
|
||||
addPaidLabel();
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const addPaidLabel = () => {
|
||||
const isExist = !!document.querySelector('#license-label')
|
||||
if (!shown.value && !isExist) {
|
||||
const spanElement = document.createElement('span');
|
||||
spanElement.textContent = $t('license.status.permanent'),
|
||||
spanElement.id = 'license-label';
|
||||
document.querySelector('#app > .settings-heading')?.appendChild(spanElement);
|
||||
}
|
||||
}
|
||||
|
||||
const isPaidAreaShown = () => {
|
||||
if (!licenseInfo.value?.isPaid) {
|
||||
return true;
|
||||
}
|
||||
const settings = LicenseStorage.getSettings(licenseInfo.value.pluginId);
|
||||
if (settings.hideLicenseAreaIfPaid) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const licenseStatusText = computed(() => {
|
||||
if (!licenseDisplayInfo.value) {
|
||||
return $t('license.status.unknown');
|
||||
}
|
||||
|
||||
if (loading.value) {
|
||||
return $t('license.status.checking');
|
||||
}
|
||||
|
||||
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) {
|
||||
status += `(<span class="text-red">${$t('license.status.expired')}</span>)`;
|
||||
return status;
|
||||
}
|
||||
|
||||
const remainingDays = licenseDisplayInfo.value.remainingDays;
|
||||
if (remainingDays !== undefined) {
|
||||
const days = $t('license.notification.days', remainingDays)
|
||||
status += `(${days})`;
|
||||
}
|
||||
|
||||
return status;
|
||||
});
|
||||
|
||||
async function refreshLicenseStatus() {
|
||||
checking.value = true;
|
||||
loading.value = true;
|
||||
// 模拟加载时间,展示 spinner 效果
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const result = await LicenseService.forceRefreshLicense();
|
||||
licenseInfo.value = result.license;
|
||||
loading.value = false;
|
||||
checking.value = false;
|
||||
}
|
||||
|
||||
function purchaseLicense() {
|
||||
const { name, email } = kintone.getLoginUser();
|
||||
const domain = licenseInfo.value?.domain;
|
||||
const pluginId = licenseInfo.value?.pluginId;
|
||||
|
||||
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() {
|
||||
const pluginId = licenseInfo.value?.pluginId;
|
||||
if (!pluginId) {
|
||||
return;
|
||||
}
|
||||
const settings = LicenseStorage.getSettings(pluginId);
|
||||
settings.hideLicenseAreaIfPaid = true;
|
||||
LicenseStorage.saveSetting(settings, pluginId);
|
||||
shown.value = false;
|
||||
addPaidLabel();
|
||||
}
|
||||
</script>
|
||||
13
src/composables/useKintoneClient.ts
Normal file
13
src/composables/useKintoneClient.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { inject } from 'vue';
|
||||
import { KintoneClientInjectionKey } from '../plugins/kintoneClient';
|
||||
import type { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||
|
||||
export function useKintoneClient(): KintoneRestAPIClient {
|
||||
const client = inject<KintoneRestAPIClient>(KintoneClientInjectionKey);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Kintone client is not provided. Make sure to install the kintoneClient plugin.');
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
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-data-aggregator-plugin"
|
||||
},
|
||||
"purchase": {
|
||||
"url": "https://alisurvey.alicorns.co.jp/s/Iuc5RxZv",
|
||||
"formIds": {
|
||||
"name": "input1761283314263",
|
||||
"email": "input1761283275767",
|
||||
"domain": "input1761283180784",
|
||||
"pluginId": "input1761283200616"
|
||||
}
|
||||
},
|
||||
"warningDaysBeforeExpiry": 7
|
||||
}
|
||||
}
|
||||
@@ -34,14 +34,14 @@
|
||||
#main-area {
|
||||
position: relative;
|
||||
}
|
||||
#main-area .kuc-spinner-1-18-0__mask {
|
||||
#main-area [class^="kuc-spinner"][class$="__mask"] {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
}
|
||||
#main-area .kuc-spinner-1-18-0__spinner {
|
||||
#main-area [class^="kuc-spinner"][class$="__spinner"] {
|
||||
position: absolute;
|
||||
}
|
||||
#main-area .kuc-spinner-1-18-0__spinner__loader {
|
||||
#main-area [class^="kuc-spinner"][class$="__spinner__loader"] {
|
||||
fill: #3498db;
|
||||
}
|
||||
|
||||
@@ -141,9 +141,9 @@
|
||||
.plugin-kuc-table > table > tbody > tr > td:first-child {
|
||||
border-left-color: #e3e7e8;
|
||||
}
|
||||
.plugin-kuc-table > table > tbody > tr > .kuc-table-1-18-0__table__body__row__action {
|
||||
height: 55px;
|
||||
align-items: center;
|
||||
.plugin-kuc-table > table > tbody > tr > td > [class^="kuc-table"][class$="__table__body__row__action-add"] {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.plugin-kuc-table:not(.condition-table) > table > tbody > tr > td:nth-child(2) {
|
||||
--kuc-table-header-1-width: 30px;
|
||||
|
||||
103
src/css/license.css
Normal file
103
src/css/license.css
Normal file
@@ -0,0 +1,103 @@
|
||||
.license-status-info {
|
||||
padding: 12px 16px;
|
||||
margin-right: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f9f9f9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.license-status-info .license-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.license-status-info .license-status-text {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.license-status-info .license-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn {
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 65px;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn:not(.main) {
|
||||
background: transparent;
|
||||
color: #666;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn:not(.main):disabled {
|
||||
background: #efefef;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn:not(.main):not(:disabled):hover {
|
||||
background: #eee;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn:not(.main):not(:disabled):active {
|
||||
background: #BDBDBD;
|
||||
border-color: #757575;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn.main {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: 1px solid #4CAF50;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn.main:hover {
|
||||
background: #388E3C;
|
||||
border-color: #388E3C;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn.main:active {
|
||||
background: #33691E;
|
||||
border-color: #33691E;
|
||||
}
|
||||
|
||||
.license-status-info .action-btn .open-icon {
|
||||
margin-left: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.notification-link {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-link:hover {
|
||||
color: #fff;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
#license-label {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 9999px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -3,4 +3,40 @@ export default {
|
||||
title: 'Data Fetch Plugin Settings',
|
||||
desc: 'Set the aggregation button name, data source app, fetch fields, filter conditions, and linking conditions, then save.',
|
||||
},
|
||||
license: {
|
||||
expiry: {
|
||||
expiryDate: 'Expiry Date {date}'
|
||||
},
|
||||
button: {
|
||||
purchase: {
|
||||
label: 'Purchase',
|
||||
desc: 'All applications under this domain can be used.'
|
||||
},
|
||||
checkLicense: {
|
||||
label: 'Check',
|
||||
desc: 'Recheck the license status.'
|
||||
},
|
||||
hide: 'Hide'
|
||||
},
|
||||
notification: {
|
||||
days: 'today | tomorrow | {count} days later',
|
||||
gotoLink: 'Please visit <a class="notification-link" href="{link}">Plugin Setting Page</a>.',
|
||||
expired: 'Your plugin {plugin} license has expired.',
|
||||
warning: 'Your plugin {plugin} license will be expired {days}.'
|
||||
},
|
||||
status: {
|
||||
label: 'License Status: ',
|
||||
checking: 'Checking license...',
|
||||
expired: 'Expired',
|
||||
expiredDisplay: 'License has expired',
|
||||
permanent: 'Permanent license',
|
||||
permanentDisplay: 'Permanent license',
|
||||
unknown: 'Unknown'
|
||||
},
|
||||
error: {
|
||||
fetchFailed: 'Remote license check failed: {e}',
|
||||
checkFailed: 'License check failed, plugin disabled: {e}',
|
||||
jwtFailed: 'JWT verification or parsing failed: {e}',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,4 +3,40 @@ export default {
|
||||
title: 'データ取得プラグインの設定',
|
||||
desc: '集約ボタン名とデータ取得元アプリ、取得フィールド、絞込条件や連結条件を設定後、保存してください。',
|
||||
},
|
||||
license: {
|
||||
expiry: {
|
||||
expiryDate: '有効期限 {date} ',
|
||||
},
|
||||
button: {
|
||||
purchase: {
|
||||
label: '購入',
|
||||
desc: '購入後、このドメイン下の全アプリで利用可能'
|
||||
},
|
||||
checkLicense: {
|
||||
label: '再確認',
|
||||
desc: 'ライセンス状態を再確認'
|
||||
},
|
||||
hide: '非表示'
|
||||
},
|
||||
notification: {
|
||||
days: '今日 | 明日 | {count}日後',
|
||||
gotoLink: 'ご利用には「<a class="notification-link" href="{link}">プラグインの設定</a>」をご確認ください。',
|
||||
expired: 'プラグイン「{plugin}」の有効期限が切れました。',
|
||||
warning: 'プラグイン「{plugin}」の試用期間が{days}で終了します。'
|
||||
},
|
||||
status: {
|
||||
label: 'ライセンス状態: ',
|
||||
checking: '確認しています...',
|
||||
expired: '期限切れ',
|
||||
expiredDisplay: 'ライセンスの有効期限が切れました',
|
||||
permanent: '永久ライセンス',
|
||||
permanentDisplay: '永久利用可能',
|
||||
unknown: '不明'
|
||||
},
|
||||
error: {
|
||||
fetchFailed: 'リモートライセンスチェックに失敗しました: {e}',
|
||||
checkFailed: 'ライセンスチェックに失敗しました。プラグイン機能が無効になりました: {e}',
|
||||
jwtFailed: 'JWTの検証または解析に失敗しました: {e}',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import type { FieldLayout, SavedData } from '@/types/model';
|
||||
import { KintoneIndexEventHandler } from './KintoneIndexEventHandler';
|
||||
import { LicenseService } from '@/services/license-service';
|
||||
|
||||
(function (PLUGIN_ID) {
|
||||
kintone.events.on('app.record.index.show', (event) => {
|
||||
try {
|
||||
const setting = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||
const config: SavedData<FieldLayout> = getConfig(setting);
|
||||
const currentApp = kintone.app.getId()?.toString();
|
||||
if (!currentApp) return;
|
||||
const handler = new KintoneIndexEventHandler(config, currentApp);
|
||||
handler.init();
|
||||
} catch (error) {
|
||||
const detailError = error instanceof Error ? '\n詳細:' + error.message : '';
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${detailError}`;
|
||||
event.error = errorMsg;
|
||||
}
|
||||
return event;
|
||||
// 授权了才能使用
|
||||
LicenseService.loadPluginIfAuthorized(PLUGIN_ID, async () => {
|
||||
try {
|
||||
const setting = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||
const config: SavedData<FieldLayout> = getConfig(setting);
|
||||
const currentApp = kintone.app.getId()?.toString();
|
||||
if (!currentApp) return;
|
||||
const handler = new KintoneIndexEventHandler(config, currentApp);
|
||||
handler.init();
|
||||
} catch (error) {
|
||||
const detailError = error instanceof Error ? '\n詳細:' + error.message : '';
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${detailError}`;
|
||||
event.error = errorMsg;
|
||||
}
|
||||
return event;
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Config設定値を変換する
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import type { FieldLayout, SavedData } from '@/types/model';
|
||||
import { KintoneIndexEventHandler } from './KintoneIndexEventHandler.mobile';
|
||||
import { LicenseService } from '@/services/license-service';
|
||||
|
||||
(function (PLUGIN_ID) {
|
||||
kintone.events.on('mobile.app.record.index.show', (event) => {
|
||||
try {
|
||||
const setting = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||
const config: SavedData<FieldLayout> = getConfig(setting);
|
||||
const currentApp = kintone.mobile.app.getId()?.toString();
|
||||
if (!currentApp) return;
|
||||
const handler = new KintoneIndexEventHandler(config, currentApp);
|
||||
handler.init();
|
||||
} catch (error) {
|
||||
const detailError = error instanceof Error ? '\n詳細:' + error.message : '';
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${detailError}`;
|
||||
event.error = errorMsg;
|
||||
}
|
||||
return event;
|
||||
// 授权了才能使用
|
||||
LicenseService.loadPluginIfAuthorized(PLUGIN_ID, async () => {
|
||||
try {
|
||||
const setting = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||
const config: SavedData<FieldLayout> = getConfig(setting);
|
||||
const currentApp = kintone.mobile.app.getId()?.toString();
|
||||
if (!currentApp) return;
|
||||
const handler = new KintoneIndexEventHandler(config, currentApp);
|
||||
handler.init();
|
||||
} catch (error) {
|
||||
const detailError = error instanceof Error ? '\n詳細:' + error.message : '';
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${detailError}`;
|
||||
event.error = errorMsg;
|
||||
}
|
||||
return event;
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Config設定値を変換する
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"js/desktop.js"
|
||||
],
|
||||
"css": [
|
||||
"css/license.css",
|
||||
"css/desktop.css"
|
||||
]
|
||||
},
|
||||
@@ -19,6 +20,7 @@
|
||||
],
|
||||
"css": [
|
||||
"css/51-modern-default.css",
|
||||
"css/license.css",
|
||||
"css/config.css"
|
||||
],
|
||||
"required_params": [
|
||||
|
||||
19
src/plugins/kintoneClient.ts
Normal file
19
src/plugins/kintoneClient.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { type App } from 'vue';
|
||||
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||
|
||||
// 创建客户端实例(单例)
|
||||
const client = new KintoneRestAPIClient();
|
||||
|
||||
// 创建注入的 Symbol(用于类型安全)
|
||||
export const KintoneClientInjectionKey = Symbol('kintone-client');
|
||||
|
||||
// 插件安装函数
|
||||
export const kintoneClientPlugin = {
|
||||
install(app: App) {
|
||||
// 提供给整个应用使用
|
||||
app.provide(KintoneClientInjectionKey, client);
|
||||
},
|
||||
};
|
||||
|
||||
// 导出客户端实例作为默认
|
||||
export default client;
|
||||
319
src/services/license-service.ts
Normal file
319
src/services/license-service.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
// 导出所有类型定义
|
||||
// 主要导出应用设置相关的类型接口
|
||||
export * from './model';
|
||||
export * from './license';
|
||||
48
src/types/license.d.ts
vendored
Normal file
48
src/types/license.d.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import { KJUR } from 'jsrsasign';
|
||||
// 许可证相关类型定义
|
||||
|
||||
export interface LicenseInfo {
|
||||
// 到期时间戳,后端需要是最后一天的 23:59:59 如果是永久许可证则为 -1
|
||||
expiredTime: number;
|
||||
// 是否是永久许可证
|
||||
isPaid: boolean;
|
||||
// 域名
|
||||
domain: string;
|
||||
// 插件ID
|
||||
pluginId: string;
|
||||
// 获取许可证的时间戳
|
||||
fetchTime: number;
|
||||
// 版本号
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface LicenseSetting {
|
||||
suppressMsgTime?: number;
|
||||
hideLicenseAreaIfPaid?: boolean;
|
||||
}
|
||||
|
||||
export interface LicenseCheckResult {
|
||||
// 是否有效(许可证真正有效可用)
|
||||
isLicenseValid: boolean;
|
||||
// 许可证信息
|
||||
license?: LicenseInfo;
|
||||
// 是否来自远程检查(从 checkLicenseRemote 返回时为 true)
|
||||
isRemote?: boolean;
|
||||
}
|
||||
|
||||
export interface PermissionConfig {
|
||||
// 是否可以管理插件(应用管理员权限)
|
||||
canManagePlugins: boolean;
|
||||
}
|
||||
|
||||
// 扩展 JWSResult,将 payloadObj 类型限定为 LicenseInfo
|
||||
export interface LicenseJWSResult extends Omit<KJUR.jws.JWS.JWSResult, 'payloadObj'> {
|
||||
payloadObj?: LicenseInfo;
|
||||
}
|
||||
|
||||
// JWT 解码后的结构
|
||||
export interface SavedLicense {
|
||||
jwt: string;
|
||||
parsed: LicenseJWSResult;
|
||||
licenseInfo: LicenseInfo;
|
||||
}
|
||||
10
src/types/my-kintone.d.ts
vendored
10
src/types/my-kintone.d.ts
vendored
@@ -1,5 +1,15 @@
|
||||
import { KintoneFormFieldProperty } from '@kintone/rest-api-client';
|
||||
|
||||
declare global {
|
||||
namespace kintone {
|
||||
/**
|
||||
* 判断当前页面是否为移动端页面
|
||||
* @returns Promise<boolean> 返回是否为移动端页面
|
||||
*/
|
||||
function isMobilePage(): Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface KucEvent<T> {
|
||||
detail: T;
|
||||
}
|
||||
|
||||
147
src/utils/license-storage.ts
Normal file
147
src/utils/license-storage.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
// 本地存储加密工具类
|
||||
import { LicenseService } from '@/services/license-service';
|
||||
import type { LicenseJWSResult, LicenseSetting, SavedLicense } from '@/types/license';
|
||||
import { KJUR } from 'jsrsasign';
|
||||
|
||||
export class LicenseStorage {
|
||||
private static readonly STORAGE_KEY_PREFIX = 'alicorns_plugin_';
|
||||
private static readonly STORAGE_SETTING_KEY_PREFIX = this.STORAGE_KEY_PREFIX + 'setting_';
|
||||
|
||||
/**
|
||||
* 生成存储key
|
||||
*/
|
||||
private static generateStorageKey(pluginId: string): string {
|
||||
return `${this.STORAGE_KEY_PREFIX}${pluginId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 JWT 到本地存储
|
||||
*/
|
||||
static saveLicense(jwt: string): SavedLicense | null {
|
||||
try {
|
||||
// 从 JWT 中提取 pluginId 以生成存储key
|
||||
if (!jwt) return null;
|
||||
|
||||
// 解码JWT
|
||||
const savedLicense = this.convertToSavedLicense(jwt);
|
||||
if (!savedLicense) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = this.generateStorageKey(savedLicense.licenseInfo.pluginId);
|
||||
localStorage.setItem(key, jwt);
|
||||
|
||||
return savedLicense;
|
||||
} catch (error) {
|
||||
console.error('Failed to save license:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储获取许可证信息
|
||||
*/
|
||||
static getLicense(pluginId: string): SavedLicense | null {
|
||||
try {
|
||||
const key = this.generateStorageKey(pluginId);
|
||||
const storedJWT = localStorage.getItem(key);
|
||||
|
||||
if (!storedJWT) return null;
|
||||
|
||||
// 解码JWT
|
||||
const savedLicense = this.convertToSavedLicense(storedJWT);
|
||||
if (!savedLicense) {
|
||||
// JWT解析失败,清理存储
|
||||
this.clearLicense(pluginId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = LicenseService.checkLicenseAvailable(savedLicense);
|
||||
if (!isValid) {
|
||||
// 获取许可证信息失败,清理存储
|
||||
this.clearLicense(pluginId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return savedLicense;
|
||||
} catch (error) {
|
||||
console.error('Failed to get license:', error);
|
||||
// 如果获取失败,清空本地存储
|
||||
this.clearLicense(pluginId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static convertToSavedLicense(storedJWT: string): SavedLicense | null {
|
||||
if (!storedJWT) return null;
|
||||
// decode
|
||||
let decodedJWT: LicenseJWSResult;
|
||||
try {
|
||||
decodedJWT = KJUR.jws.JWS.parse(storedJWT) as LicenseJWSResult;
|
||||
if (!decodedJWT) {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get license:', error);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
jwt: storedJWT,
|
||||
parsed: decodedJWT,
|
||||
licenseInfo: decodedJWT.payloadObj!,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除许可证信息
|
||||
*/
|
||||
static clearLicense(pluginId: string): void {
|
||||
const key = this.generateStorageKey(pluginId);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成存储key
|
||||
*/
|
||||
private static generateSettingStorageKey(pluginId: string): string {
|
||||
return `${this.STORAGE_SETTING_KEY_PREFIX}${pluginId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存设置信息到本地存储
|
||||
*/
|
||||
static saveSetting(setting: LicenseSetting, pluginId: string): void {
|
||||
try {
|
||||
const key = this.generateSettingStorageKey(pluginId);
|
||||
localStorage.setItem(key, JSON.stringify(setting));
|
||||
} catch (error) {
|
||||
console.error('Failed to save setting:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储获取设置信息
|
||||
*/
|
||||
static getSettings(pluginId: string): LicenseSetting {
|
||||
try {
|
||||
const key = this.generateSettingStorageKey(pluginId);
|
||||
const storedData = localStorage.getItem(key);
|
||||
|
||||
if (!storedData) return {};
|
||||
|
||||
return JSON.parse(storedData);
|
||||
} catch (error) {
|
||||
console.error('Failed to get setting:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除设置信息
|
||||
*/
|
||||
static clearSetting(pluginId: string): void {
|
||||
const key = this.generateSettingStorageKey(pluginId);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
51
src/utils/permissions.ts
Normal file
51
src/utils/permissions.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// 权限检查工具类
|
||||
import type { PermissionConfig } from '@/types/license';
|
||||
import client from '@/plugins/kintoneClient.ts'
|
||||
|
||||
export class PermissionService {
|
||||
/**
|
||||
* 检查用户是否有管理插件的权限
|
||||
* 基于kintone的权限系统判断用户是否可以管理应用插件
|
||||
*/
|
||||
static async checkPermissions(): Promise<PermissionConfig> {
|
||||
try {
|
||||
// 获取应用信息
|
||||
const appId = kintone.app.getId() || kintone.mobile.app.getId();
|
||||
if (!appId) {
|
||||
return this.getDefaultPermissions();
|
||||
}
|
||||
|
||||
// 检查应用的管理权限
|
||||
const hasAdminRights = await this.checkAppAdminRights(appId);
|
||||
|
||||
return {
|
||||
canManagePlugins: hasAdminRights,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to check permissions:', error);
|
||||
return this.getDefaultPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查应用管理员权限
|
||||
*/
|
||||
private static async checkAppAdminRights(appId: number): Promise<boolean> {
|
||||
try {
|
||||
await client.app.getAppAcl({ app: appId });
|
||||
return true;
|
||||
} catch (error) {
|
||||
// 如果API调用失败,默认认为没有权限
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认权限配置(无权限)
|
||||
*/
|
||||
private static getDefaultPermissions(): PermissionConfig {
|
||||
return {
|
||||
canManagePlugins: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user