This commit is contained in:
2025-10-24 12:53:45 +08:00
parent e86afd8487
commit 767af75aad
4 changed files with 70 additions and 330 deletions

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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!,
}
}
/** /**
* 清除许可证信息 * 清除许可证信息
*/ */