171 lines
4.7 KiB
TypeScript
171 lines
4.7 KiB
TypeScript
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(),
|
|
}
|
|
}
|
|
}
|