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

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

View File

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

View File

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