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 {
|
||||
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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
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 {
|
||||
@@ -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<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 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!,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除许可证信息
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user