From 767af75aadbb0c38704e273523c450cebe8bd204 Mon Sep 17 00:00:00 2001 From: xuejiahao Date: Fri, 24 Oct 2025 12:53:45 +0800 Subject: [PATCH] fix --- src/css/license.css | 275 +------------------------------- src/services/license-service.ts | 46 +++--- src/types/license.d.ts | 30 ++-- src/utils/license-storage.ts | 49 ++++-- 4 files changed, 70 insertions(+), 330 deletions(-) diff --git a/src/css/license.css b/src/css/license.css index 61f0dda..1210523 100644 --- a/src/css/license.css +++ b/src/css/license.css @@ -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 { padding: 12px 16px; margin-right: 24px; @@ -344,11 +72,12 @@ .notification-link { color: #fff; + opacity: 0.8; text-decoration: underline; } .notification-link:hover { - color: #daf0ff; + opacity: 1; } .text-red { diff --git a/src/services/license-service.ts b/src/services/license-service.ts index e1639a1..3fdef8c 100644 --- a/src/services/license-service.ts +++ b/src/services/license-service.ts @@ -1,4 +1,4 @@ -import type { LicenseInfo, LicenseCheckResult } from '@/types/license'; +import type { LicenseInfo, LicenseCheckResult, SavedLicense } from '@/types/license'; import i18n from '@/i18n'; import { LicenseStorage } from '@/utils/license-storage'; import { PermissionService } from '@/utils/permissions'; @@ -46,21 +46,20 @@ export class LicenseService { * 检查许可证是否有效 * 参数是解码后的JWT结构,验证签名和payload进行原有检查 */ - static checkLicenseAvailable(jwtString: string, decodedJWT: KJUR.jws.JWS.JWSResult): boolean { + static checkLicenseAvailable(savedLicense: SavedLicense): boolean { try { // 验证完整的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) { console.warn('JWT signature verification failed'); return false; } - } catch (error) { console.error('JWT verification or parsing failed:', error); return false; } - // JWT验证通过,从payloadPP解析license进行原有检查 - const license: LicenseInfo = JSON.parse(decodedJWT.payloadPP); + + const license = savedLicense.licenseInfo; const domain = this.getDomain() const pluginId = this.getPluginId() @@ -127,18 +126,21 @@ export class LicenseService { const jwt = response.jwt; - // 保存 JWT 到本地存储,获取解码后的结构 - const decodedJWT = LicenseStorage.saveLicense(jwt); + // 保存 JWT 到本地存储,获取保存的许可证结构 + const savedLicense = LicenseStorage.saveLicense(jwt); + if (!savedLicense) { + return { + isLicenseValid: false, + isRemote: true, + }; + } - // 从解码后的JWT提取许可证信息 - const license: LicenseInfo = JSON.parse(decodedJWT.payloadPP); - - // 使用解码后的JWT进行验证 - const isValid = this.checkLicenseAvailable(jwt, decodedJWT); + // 使用解析后的JWT进行验证 + const isValid = this.checkLicenseAvailable(savedLicense); return { isLicenseValid: isValid, - license, + license: savedLicense.licenseInfo, isRemote: true, }; @@ -270,15 +272,10 @@ export class LicenseService { * 获取许可证信息(用于显示) */ static getLocalLicenseInfo(): LicenseInfo | null { - const decodedJWT = LicenseStorage.getLicense(this.getPluginId()); - if (!decodedJWT) return null; + const savedLicense = LicenseStorage.getLicense(this.getPluginId()); + if (!savedLicense) return null; - try { - return JSON.parse(decodedJWT.payloadPP) as LicenseInfo; - } catch (error) { - console.error('Failed to parse payloadPP from decoded JWT:', error); - return null; - } + return savedLicense.licenseInfo; } /** @@ -341,11 +338,6 @@ export class LicenseService { const license = this.mockCreateTrialLicense(); const jwt = this.generateJWT(license); - // 模拟有时创建失败的情况(1%概率) - if (Math.random() < 0.01) { - return { success: false }; - } - return { success: true, jwt, diff --git a/src/types/license.d.ts b/src/types/license.d.ts index 7cdf7a2..08102db 100644 --- a/src/types/license.d.ts +++ b/src/types/license.d.ts @@ -1,3 +1,4 @@ +import { KJUR } from 'jsrsasign'; // 许可证相关类型定义 export interface LicenseInfo { @@ -29,24 +30,19 @@ export interface LicenseCheckResult { isRemote?: boolean; } -export interface NotificationConfig { - // 显示通知 - show: boolean; - // 通知类型 - type: 'expiry' | 'warning' | 'error'; - // 通知标题 - title: string; - // 通知内容 - message: string; - // 是否有购买按钮 - showPurchaseButton: boolean; - // 是否有检查按钮 - showCheckButton: boolean; - // 不再提醒选项 - showDontShowAgain: boolean; -} - export interface PermissionConfig { // 是否可以管理插件(应用管理员权限) canManagePlugins: boolean; } + +// 扩展 JWSResult,将 payloadObj 类型限定为 LicenseInfo +export interface LicenseJWSResult extends Omit { + payloadObj?: LicenseInfo; +} + +// JWT 解码后的结构 +export interface SavedLicense { + jwt: string; + parsed: LicenseJWSResult; + licenseInfo: LicenseInfo; +} diff --git a/src/utils/license-storage.ts b/src/utils/license-storage.ts index b27fbe8..e9986eb 100644 --- a/src/utils/license-storage.ts +++ b/src/utils/license-storage.ts @@ -1,6 +1,6 @@ // 本地存储加密工具类 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'; export class LicenseStorage { @@ -29,18 +29,21 @@ export class LicenseStorage { /** * 保存 JWT 到本地存储 */ - static saveLicense(jwt: string) { + static saveLicense(jwt: string): SavedLicense | null { try { // 从 JWT 中提取 pluginId 以生成存储key - const decoded = this.parseJWT(jwt); - if (!decoded) { - throw new Error('Failed to parse JWT'); + if (!jwt) return null; + + // 解码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); - return decoded; + return savedLicense; } catch (error) { console.error('Failed to save license:', error); throw error; @@ -50,7 +53,7 @@ export class LicenseStorage { /** * 从本地存储获取许可证信息 */ - static getLicense(pluginId: string) { + static getLicense(pluginId: string): SavedLicense | null { try { const key = this.generateStorageKey(pluginId); const storedJWT = localStorage.getItem(key); @@ -58,21 +61,21 @@ export class LicenseStorage { if (!storedJWT) return null; // 解码JWT - const decodedJWT = this.parseJWT(storedJWT); - if (!decodedJWT) { + const savedLicense = this.convertToSavedLicense(storedJWT); + if (!savedLicense) { // JWT解析失败,清理存储 this.clearLicense(pluginId); return null; } - const isValid = LicenseService.checkLicenseAvailable(storedJWT, decodedJWT); + const isValid = LicenseService.checkLicenseAvailable(savedLicense); if (!isValid) { // 获取许可证信息失败,清理存储 this.clearLicense(pluginId); return null; } - return decodedJWT; + return savedLicense; } catch (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!, + } + } + /** * 清除许可证信息 */