fix
This commit is contained in:
@@ -1,275 +1,3 @@
|
|||||||
/* 许可证弹框样式 */
|
|
||||||
.license-modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 10000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
min-width: 400px;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .modal-header {
|
|
||||||
padding: 20px 24px 0 24px;
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .modal-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .modal-body {
|
|
||||||
padding: 0 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .modal-footer {
|
|
||||||
padding: 20px 24px;
|
|
||||||
border-top: 1px solid #e5e5e5;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
color: #666;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .btn-primary {
|
|
||||||
background: #007acc;
|
|
||||||
color: white;
|
|
||||||
border-color: #007acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .btn-primary:hover {
|
|
||||||
background: #005aa3;
|
|
||||||
border-color: #005aa3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .btn-secondary:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .expiry-modal {
|
|
||||||
/* 样式已在content中定义 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .expiry-info .expiry-message {
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .info-item {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .info-item .label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
min-width: 100px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .info-item .value {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-modal-content .purchase-link {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 许可证通知样式 */
|
|
||||||
.license-notification {
|
|
||||||
position: fixed;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 9999;
|
|
||||||
min-width: 320px;
|
|
||||||
max-width: 450px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .notification-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
animation: slideIn 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .notification-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 16px 8px 16px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .notification-title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .icon-warning {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .close-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #999;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .close-btn:hover {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .notification-message {
|
|
||||||
padding: 12px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .notification-actions {
|
|
||||||
padding: 8px 16px 16px 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .dont-show-again {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .dont-show-again input {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .btn {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
color: #666;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .btn-primary {
|
|
||||||
background: #007acc;
|
|
||||||
color: white;
|
|
||||||
border-color: #007acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .btn-primary:hover {
|
|
||||||
background: #005aa3;
|
|
||||||
border-color: #005aa3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-notification .btn-secondary:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 许可证状态显示样式 */
|
|
||||||
.license-status-error,
|
|
||||||
.license-status {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-status-error {
|
|
||||||
background-color: #fff5f5;
|
|
||||||
border-color: #feb2b2;
|
|
||||||
color: #c53030;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-status .license-error {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-status .license-error h3 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
color: #c53030;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-status .license-error p {
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-status .license-refresh-btn {
|
|
||||||
background: #e53e3e;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-status .license-refresh-btn:hover {
|
|
||||||
background: #c53030;
|
|
||||||
}
|
|
||||||
|
|
||||||
.license-status-info {
|
.license-status-info {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
@@ -344,11 +72,12 @@
|
|||||||
|
|
||||||
.notification-link {
|
.notification-link {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
opacity: 0.8;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-link:hover {
|
.notification-link:hover {
|
||||||
color: #daf0ff;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-red {
|
.text-red {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { LicenseInfo, LicenseCheckResult } from '@/types/license';
|
import type { LicenseInfo, LicenseCheckResult, SavedLicense } from '@/types/license';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
import { LicenseStorage } from '@/utils/license-storage';
|
import { LicenseStorage } from '@/utils/license-storage';
|
||||||
import { PermissionService } from '@/utils/permissions';
|
import { PermissionService } from '@/utils/permissions';
|
||||||
@@ -46,21 +46,20 @@ export class LicenseService {
|
|||||||
* 检查许可证是否有效
|
* 检查许可证是否有效
|
||||||
* 参数是解码后的JWT结构,验证签名和payload进行原有检查
|
* 参数是解码后的JWT结构,验证签名和payload进行原有检查
|
||||||
*/
|
*/
|
||||||
static checkLicenseAvailable(jwtString: string, decodedJWT: KJUR.jws.JWS.JWSResult): boolean {
|
static checkLicenseAvailable(savedLicense: SavedLicense): boolean {
|
||||||
try {
|
try {
|
||||||
// 验证完整的JWT
|
// 验证完整的JWT
|
||||||
const result = KJUR.jws.JWS.verifyJWT(jwtString, rsaPublicKey.trim(), {alg: ['RS256']});
|
const result = KJUR.jws.JWS.verifyJWT(savedLicense.jwt, rsaPublicKey.trim(), {alg: ['RS256']});
|
||||||
if (!result) {
|
if (!result) {
|
||||||
console.warn('JWT signature verification failed');
|
console.warn('JWT signature verification failed');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('JWT verification or parsing failed:', error);
|
console.error('JWT verification or parsing failed:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// JWT验证通过,从payloadPP解析license进行原有检查
|
|
||||||
const license: LicenseInfo = JSON.parse(decodedJWT.payloadPP);
|
const license = savedLicense.licenseInfo;
|
||||||
|
|
||||||
const domain = this.getDomain()
|
const domain = this.getDomain()
|
||||||
const pluginId = this.getPluginId()
|
const pluginId = this.getPluginId()
|
||||||
@@ -127,18 +126,21 @@ export class LicenseService {
|
|||||||
|
|
||||||
const jwt = response.jwt;
|
const jwt = response.jwt;
|
||||||
|
|
||||||
// 保存 JWT 到本地存储,获取解码后的结构
|
// 保存 JWT 到本地存储,获取保存的许可证结构
|
||||||
const decodedJWT = LicenseStorage.saveLicense(jwt);
|
const savedLicense = LicenseStorage.saveLicense(jwt);
|
||||||
|
if (!savedLicense) {
|
||||||
|
return {
|
||||||
|
isLicenseValid: false,
|
||||||
|
isRemote: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 从解码后的JWT提取许可证信息
|
// 使用解析后的JWT进行验证
|
||||||
const license: LicenseInfo = JSON.parse(decodedJWT.payloadPP);
|
const isValid = this.checkLicenseAvailable(savedLicense);
|
||||||
|
|
||||||
// 使用解码后的JWT进行验证
|
|
||||||
const isValid = this.checkLicenseAvailable(jwt, decodedJWT);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLicenseValid: isValid,
|
isLicenseValid: isValid,
|
||||||
license,
|
license: savedLicense.licenseInfo,
|
||||||
isRemote: true,
|
isRemote: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,15 +272,10 @@ export class LicenseService {
|
|||||||
* 获取许可证信息(用于显示)
|
* 获取许可证信息(用于显示)
|
||||||
*/
|
*/
|
||||||
static getLocalLicenseInfo(): LicenseInfo | null {
|
static getLocalLicenseInfo(): LicenseInfo | null {
|
||||||
const decodedJWT = LicenseStorage.getLicense(this.getPluginId());
|
const savedLicense = LicenseStorage.getLicense(this.getPluginId());
|
||||||
if (!decodedJWT) return null;
|
if (!savedLicense) return null;
|
||||||
|
|
||||||
try {
|
return savedLicense.licenseInfo;
|
||||||
return JSON.parse(decodedJWT.payloadPP) as LicenseInfo;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to parse payloadPP from decoded JWT:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -341,11 +338,6 @@ export class LicenseService {
|
|||||||
const license = this.mockCreateTrialLicense();
|
const license = this.mockCreateTrialLicense();
|
||||||
const jwt = this.generateJWT(license);
|
const jwt = this.generateJWT(license);
|
||||||
|
|
||||||
// 模拟有时创建失败的情况(1%概率)
|
|
||||||
if (Math.random() < 0.01) {
|
|
||||||
return { success: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
jwt,
|
jwt,
|
||||||
|
|||||||
30
src/types/license.d.ts
vendored
30
src/types/license.d.ts
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
import { KJUR } from 'jsrsasign';
|
||||||
// 许可证相关类型定义
|
// 许可证相关类型定义
|
||||||
|
|
||||||
export interface LicenseInfo {
|
export interface LicenseInfo {
|
||||||
@@ -29,24 +30,19 @@ export interface LicenseCheckResult {
|
|||||||
isRemote?: boolean;
|
isRemote?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationConfig {
|
|
||||||
// 显示通知
|
|
||||||
show: boolean;
|
|
||||||
// 通知类型
|
|
||||||
type: 'expiry' | 'warning' | 'error';
|
|
||||||
// 通知标题
|
|
||||||
title: string;
|
|
||||||
// 通知内容
|
|
||||||
message: string;
|
|
||||||
// 是否有购买按钮
|
|
||||||
showPurchaseButton: boolean;
|
|
||||||
// 是否有检查按钮
|
|
||||||
showCheckButton: boolean;
|
|
||||||
// 不再提醒选项
|
|
||||||
showDontShowAgain: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PermissionConfig {
|
export interface PermissionConfig {
|
||||||
// 是否可以管理插件(应用管理员权限)
|
// 是否可以管理插件(应用管理员权限)
|
||||||
canManagePlugins: boolean;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// 本地存储加密工具类
|
// 本地存储加密工具类
|
||||||
import { LicenseService } from '@/services/license-service';
|
import { LicenseService } from '@/services/license-service';
|
||||||
import type { LicenseInfo, LicenseSetting } from '@/types/license';
|
import type { LicenseInfo, LicenseJWSResult, LicenseSetting, SavedLicense } from '@/types/license';
|
||||||
import { KJUR } from 'jsrsasign';
|
import { KJUR } from 'jsrsasign';
|
||||||
|
|
||||||
export class LicenseStorage {
|
export class LicenseStorage {
|
||||||
@@ -29,18 +29,21 @@ export class LicenseStorage {
|
|||||||
/**
|
/**
|
||||||
* 保存 JWT 到本地存储
|
* 保存 JWT 到本地存储
|
||||||
*/
|
*/
|
||||||
static saveLicense(jwt: string) {
|
static saveLicense(jwt: string): SavedLicense | null {
|
||||||
try {
|
try {
|
||||||
// 从 JWT 中提取 pluginId 以生成存储key
|
// 从 JWT 中提取 pluginId 以生成存储key
|
||||||
const decoded = this.parseJWT(jwt);
|
if (!jwt) return null;
|
||||||
if (!decoded) {
|
|
||||||
throw new Error('Failed to parse JWT');
|
// 解码JWT
|
||||||
|
const savedLicense = this.convertToSavedLicense(jwt);
|
||||||
|
if (!savedLicense) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
const licenseInfo = JSON.parse(decoded.payloadPP) as LicenseInfo;
|
|
||||||
const key = this.generateStorageKey(licenseInfo.pluginId);
|
const key = this.generateStorageKey(savedLicense.licenseInfo.pluginId);
|
||||||
localStorage.setItem(key, jwt);
|
localStorage.setItem(key, jwt);
|
||||||
|
|
||||||
return decoded;
|
return savedLicense;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save license:', error);
|
console.error('Failed to save license:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -50,7 +53,7 @@ export class LicenseStorage {
|
|||||||
/**
|
/**
|
||||||
* 从本地存储获取许可证信息
|
* 从本地存储获取许可证信息
|
||||||
*/
|
*/
|
||||||
static getLicense(pluginId: string) {
|
static getLicense(pluginId: string): SavedLicense | null {
|
||||||
try {
|
try {
|
||||||
const key = this.generateStorageKey(pluginId);
|
const key = this.generateStorageKey(pluginId);
|
||||||
const storedJWT = localStorage.getItem(key);
|
const storedJWT = localStorage.getItem(key);
|
||||||
@@ -58,21 +61,21 @@ export class LicenseStorage {
|
|||||||
if (!storedJWT) return null;
|
if (!storedJWT) return null;
|
||||||
|
|
||||||
// 解码JWT
|
// 解码JWT
|
||||||
const decodedJWT = this.parseJWT(storedJWT);
|
const savedLicense = this.convertToSavedLicense(storedJWT);
|
||||||
if (!decodedJWT) {
|
if (!savedLicense) {
|
||||||
// JWT解析失败,清理存储
|
// JWT解析失败,清理存储
|
||||||
this.clearLicense(pluginId);
|
this.clearLicense(pluginId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = LicenseService.checkLicenseAvailable(storedJWT, decodedJWT);
|
const isValid = LicenseService.checkLicenseAvailable(savedLicense);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
// 获取许可证信息失败,清理存储
|
// 获取许可证信息失败,清理存储
|
||||||
this.clearLicense(pluginId);
|
this.clearLicense(pluginId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return decodedJWT;
|
return savedLicense;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get license:', error);
|
console.error('Failed to get license:', error);
|
||||||
// 如果获取失败,清空本地存储
|
// 如果获取失败,清空本地存储
|
||||||
@@ -81,6 +84,26 @@ export class LicenseStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static convertToSavedLicense(storedJWT: string): SavedLicense | null {
|
||||||
|
if (!storedJWT) return null;
|
||||||
|
// decode
|
||||||
|
let decodedJWT: LicenseJWSResult;
|
||||||
|
try {
|
||||||
|
decodedJWT = this.parseJWT(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!,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除许可证信息
|
* 清除许可证信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user