feat: build licensed desktop image processor
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { app } from 'electron'
|
||||
import { createHash } from 'node:crypto'
|
||||
import os from 'node:os'
|
||||
import { machineIdSync } from 'node-machine-id'
|
||||
import type {
|
||||
AppInfo,
|
||||
LicenseSession,
|
||||
LicenseViewState,
|
||||
VersionInfo,
|
||||
} from '../../src/shared/types'
|
||||
import { LicenseStore, type StoredLicense } from './license-store'
|
||||
import {
|
||||
DesktopServerApi,
|
||||
type DevicePayload,
|
||||
ServerApiError,
|
||||
} from './server-api'
|
||||
|
||||
const OFFLINE_GRACE_MS = 72 * 60 * 60 * 1000
|
||||
|
||||
export class LicenseManager {
|
||||
private readonly store = new LicenseStore()
|
||||
private readonly server = new DesktopServerApi()
|
||||
|
||||
getAppInfo(): AppInfo {
|
||||
return {
|
||||
name: '正版情趣王',
|
||||
version: app.getVersion(),
|
||||
platform: process.platform,
|
||||
deviceName: os.hostname(),
|
||||
}
|
||||
}
|
||||
|
||||
async getState(): Promise<LicenseViewState> {
|
||||
const stored = await this.store.load()
|
||||
if (!stored) {
|
||||
return {
|
||||
status: 'unlicensed',
|
||||
message: '输入激活码后即可开始使用',
|
||||
offline: false,
|
||||
}
|
||||
}
|
||||
return this.validateStored(stored)
|
||||
}
|
||||
|
||||
async activate(activationCode: string): Promise<LicenseViewState> {
|
||||
try {
|
||||
const session = await this.server.activate(activationCode, this.devicePayload())
|
||||
if (!session.token) {
|
||||
throw new ServerApiError('INVALID_RESPONSE', '授权服务未返回设备凭据')
|
||||
}
|
||||
await this.persistSession(session)
|
||||
return {
|
||||
status: 'active',
|
||||
message: '设备已激活',
|
||||
offline: false,
|
||||
session,
|
||||
version: session.version,
|
||||
}
|
||||
} catch (error) {
|
||||
return this.errorState(error)
|
||||
}
|
||||
}
|
||||
|
||||
async validate(): Promise<LicenseViewState> {
|
||||
const stored = await this.store.load()
|
||||
if (!stored) {
|
||||
return {
|
||||
status: 'unlicensed',
|
||||
message: '当前设备尚未激活',
|
||||
offline: false,
|
||||
}
|
||||
}
|
||||
return this.validateStored(stored)
|
||||
}
|
||||
|
||||
async clear(): Promise<LicenseViewState> {
|
||||
await this.store.clear()
|
||||
return {
|
||||
status: 'unlicensed',
|
||||
message: '本机授权凭据已清除',
|
||||
offline: false,
|
||||
}
|
||||
}
|
||||
|
||||
checkVersion(): Promise<VersionInfo> {
|
||||
return this.server.checkVersion(app.getVersion())
|
||||
}
|
||||
|
||||
private async validateStored(stored: StoredLicense): Promise<LicenseViewState> {
|
||||
try {
|
||||
const session = await this.server.login(stored.token, this.devicePayload())
|
||||
session.token = stored.token
|
||||
await this.persistSession(session)
|
||||
return {
|
||||
status: 'active',
|
||||
message: '授权有效',
|
||||
offline: false,
|
||||
session,
|
||||
version: session.version,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ServerApiError && error.code.startsWith('NETWORK_')
|
||||
&& this.canUseOffline(stored)) {
|
||||
const session: LicenseSession = {
|
||||
licenseCodeMask: stored.licenseCodeMask,
|
||||
deviceMask: stored.deviceMask,
|
||||
expiresAt: stored.expiresAt,
|
||||
maxDevices: stored.maxDevices,
|
||||
}
|
||||
return {
|
||||
status: 'active',
|
||||
message: '当前处于离线授权模式,请在 72 小时内联网校验',
|
||||
offline: true,
|
||||
session,
|
||||
}
|
||||
}
|
||||
return this.errorState(error)
|
||||
}
|
||||
}
|
||||
|
||||
private async persistSession(session: LicenseSession): Promise<void> {
|
||||
if (!session.token) {
|
||||
throw new Error('缺少设备授权凭据')
|
||||
}
|
||||
await this.store.save({
|
||||
token: session.token,
|
||||
licenseCodeMask: session.licenseCodeMask,
|
||||
deviceMask: session.deviceMask,
|
||||
expiresAt: session.expiresAt,
|
||||
maxDevices: session.maxDevices,
|
||||
lastVerifiedAt: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
private canUseOffline(stored: StoredLicense): boolean {
|
||||
const lastVerifiedAt = new Date(stored.lastVerifiedAt).getTime()
|
||||
const expiresAt = stored.expiresAt ? new Date(stored.expiresAt).getTime() : Number.POSITIVE_INFINITY
|
||||
return Number.isFinite(lastVerifiedAt)
|
||||
&& Date.now() - lastVerifiedAt <= OFFLINE_GRACE_MS
|
||||
&& Date.now() < expiresAt
|
||||
}
|
||||
|
||||
private errorState(error: unknown): LicenseViewState {
|
||||
if (error instanceof ServerApiError) {
|
||||
return {
|
||||
status: error.code.startsWith('NETWORK_') ? 'network-error' : 'invalid',
|
||||
message: error.message,
|
||||
offline: false,
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'invalid',
|
||||
message: error instanceof Error ? error.message : '授权校验失败',
|
||||
offline: false,
|
||||
}
|
||||
}
|
||||
|
||||
private devicePayload(): DevicePayload {
|
||||
const deviceId = createHash('sha256')
|
||||
.update(`正版情趣王:${machineIdSync()}`)
|
||||
.digest('hex')
|
||||
return {
|
||||
deviceId,
|
||||
deviceName: os.hostname(),
|
||||
osName: `${os.type()} ${os.arch()}`,
|
||||
osVersion: os.release(),
|
||||
appVersion: app.getVersion(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user