mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
306 lines
9.3 KiB
TypeScript
306 lines
9.3 KiB
TypeScript
import { Service } from '../../shared/kernel'
|
|
import { MainContext } from '../context'
|
|
import { BrowserWindow, webContents } from 'electron'
|
|
import type { IpcMainInvokeEvent } from 'electron'
|
|
import type { settingsKey, settingsSpec, settingChange } from '../../preload/types'
|
|
import type { permissionLevel } from './PermissionService'
|
|
import { SettingEntity } from '../db/entities'
|
|
|
|
declare module '../../shared/kernel' {
|
|
interface Context {
|
|
settings: SettingsService
|
|
}
|
|
}
|
|
|
|
type settingValueKind = 'string' | 'boolean' | 'number' | 'json'
|
|
|
|
type settingDefinition = {
|
|
kind: settingValueKind
|
|
defaultValue: unknown
|
|
readPermission?: permissionLevel | 'any'
|
|
writePermission?: permissionLevel | 'any'
|
|
validate?: (value: unknown) => boolean
|
|
normalize?: (value: unknown) => unknown
|
|
onChanged?: (ctx: MainContext, next: unknown, prev: unknown) => void
|
|
}
|
|
|
|
export class SettingsService extends Service {
|
|
constructor(ctx: MainContext) {
|
|
super(ctx, 'settings')
|
|
this.registerIpc()
|
|
}
|
|
|
|
private get mainCtx() {
|
|
return this.ctx as MainContext
|
|
}
|
|
|
|
private definitions: Record<settingsKey, settingDefinition> = {
|
|
is_wizard_completed: {
|
|
kind: 'boolean',
|
|
defaultValue: false,
|
|
writePermission: 'any'
|
|
},
|
|
log_level: {
|
|
kind: 'string',
|
|
defaultValue: 'info',
|
|
writePermission: 'admin',
|
|
validate: (v) => v === 'debug' || v === 'info' || v === 'warn' || v === 'error',
|
|
onChanged: (ctx, next) => {
|
|
ctx.logger.setLevel(next as any)
|
|
}
|
|
},
|
|
window_zoom: {
|
|
kind: 'number',
|
|
defaultValue: 1.0,
|
|
writePermission: 'admin',
|
|
onChanged: (_ctx, next) => {
|
|
const zoom = Number(next) || 1.0
|
|
webContents.getAllWebContents().forEach((wc: any) => {
|
|
wc.setZoomFactor(zoom)
|
|
})
|
|
}
|
|
},
|
|
window_theme: {
|
|
kind: 'string',
|
|
defaultValue: 'auto',
|
|
writePermission: 'admin',
|
|
validate: (v) => v === 'auto' || v === 'dark' || v === 'light'
|
|
},
|
|
window_effect: {
|
|
kind: 'string',
|
|
defaultValue: 'mica',
|
|
writePermission: 'admin',
|
|
validate: (v) =>
|
|
['mica', 'tabbed', 'acrylic', 'blur', 'transparent', 'none'].includes(v as string)
|
|
},
|
|
window_radius: {
|
|
kind: 'string',
|
|
defaultValue: 'rounded',
|
|
writePermission: 'admin',
|
|
validate: (v) => v === 'rounded' || v === 'small' || v === 'square'
|
|
},
|
|
auto_score_enabled: {
|
|
kind: 'boolean',
|
|
defaultValue: false,
|
|
writePermission: 'admin'
|
|
},
|
|
auto_score_rules: {
|
|
kind: 'json',
|
|
defaultValue: [],
|
|
writePermission: 'admin'
|
|
},
|
|
current_theme_id: {
|
|
kind: 'string',
|
|
defaultValue: 'light-default',
|
|
writePermission: 'admin'
|
|
},
|
|
pg_connection_string: {
|
|
kind: 'string',
|
|
defaultValue: '',
|
|
writePermission: 'admin'
|
|
},
|
|
pg_connection_status: {
|
|
kind: 'json',
|
|
defaultValue: { connected: false, type: 'sqlite' },
|
|
writePermission: 'admin'
|
|
}
|
|
}
|
|
|
|
private cache = new Map<string, string>()
|
|
private initPromise: Promise<void> | null = null
|
|
|
|
async initialize(): Promise<void> {
|
|
if (this.initPromise) return this.initPromise
|
|
this.initPromise = (async () => {
|
|
await this.loadCache()
|
|
await this.ensureDefaults()
|
|
})()
|
|
return this.initPromise
|
|
}
|
|
|
|
private async loadCache() {
|
|
const repo = this.ctx.db.dataSource.getRepository(SettingEntity)
|
|
const rows = await repo.find()
|
|
this.cache.clear()
|
|
for (const r of rows) this.cache.set(r.key, String(r.value ?? ''))
|
|
}
|
|
|
|
private async ensureDefaults() {
|
|
const repo = this.ctx.db.dataSource.getRepository(SettingEntity)
|
|
for (const key of Object.keys(this.definitions) as settingsKey[]) {
|
|
if (this.cache.has(key)) continue
|
|
const def = this.definitions[key]
|
|
const raw = this.serializeValue(key, def.defaultValue)
|
|
await repo
|
|
.createQueryBuilder()
|
|
.insert()
|
|
.into(SettingEntity)
|
|
.values({ key, value: raw })
|
|
.orIgnore()
|
|
.execute()
|
|
this.cache.set(key, raw)
|
|
}
|
|
}
|
|
|
|
private serializeValue(key: settingsKey, value: unknown): string {
|
|
const def = this.definitions[key]
|
|
switch (def.kind) {
|
|
case 'boolean':
|
|
return value ? '1' : '0'
|
|
case 'number':
|
|
return String(value)
|
|
case 'json':
|
|
return JSON.stringify(value)
|
|
case 'string':
|
|
default:
|
|
return String(value)
|
|
}
|
|
}
|
|
|
|
private deserializeValue(key: settingsKey, raw: string): unknown {
|
|
const def = this.definitions[key]
|
|
switch (def.kind) {
|
|
case 'boolean':
|
|
return raw === '1' || raw.toLowerCase() === 'true'
|
|
case 'number':
|
|
return Number(raw)
|
|
case 'json':
|
|
try {
|
|
return JSON.parse(raw)
|
|
} catch {
|
|
return def.defaultValue
|
|
}
|
|
case 'string':
|
|
default:
|
|
return raw
|
|
}
|
|
}
|
|
|
|
private parseValue(key: settingsKey, raw: string | undefined): unknown {
|
|
const def = this.definitions[key]
|
|
if (raw == null) return def.defaultValue
|
|
const deserialized = this.deserializeValue(key, raw)
|
|
const normalized = def.normalize ? def.normalize(deserialized) : deserialized
|
|
if (def.validate && !def.validate(normalized)) return def.defaultValue
|
|
return normalized
|
|
}
|
|
|
|
private canWrite(event: IpcMainInvokeEvent, key: settingsKey): boolean {
|
|
const required = this.definitions[key].writePermission
|
|
if (!required || required === 'any') return true
|
|
return this.mainCtx.permissions.requirePermission(event, required)
|
|
}
|
|
|
|
private notifyChanged(key: settingsKey, value: unknown) {
|
|
const change: settingChange = { key, value } as any
|
|
const windows = BrowserWindow.getAllWindows()
|
|
for (const win of windows) {
|
|
win.webContents.send('settings:changed', change)
|
|
}
|
|
}
|
|
|
|
private registerIpc() {
|
|
this.mainCtx.handle('settings:getAll', async () => {
|
|
await this.initialize()
|
|
return { success: true, data: this.getAll() }
|
|
})
|
|
this.mainCtx.handle('settings:get', async (_event, key: settingsKey) => {
|
|
await this.initialize()
|
|
return { success: true, data: this.getValue(key) }
|
|
})
|
|
this.mainCtx.handle('settings:set', async (event, key: settingsKey, value: any) => {
|
|
if (!this.canWrite(event, key)) return { success: false, message: 'Permission denied' }
|
|
await this.setValue(key, value)
|
|
return { success: true }
|
|
})
|
|
}
|
|
|
|
async reloadFromDb(options: { notify?: boolean } = {}): Promise<void> {
|
|
await this.initialize()
|
|
const notify = options.notify ?? true
|
|
const prev = this.getAll()
|
|
await this.loadCache()
|
|
await this.ensureDefaults()
|
|
if (!notify) return
|
|
|
|
for (const key of Object.keys(this.definitions) as settingsKey[]) {
|
|
const def = this.definitions[key]
|
|
const next = this.getValue(key as any) as any
|
|
const prevValue = prev[key]
|
|
if (next !== prevValue) {
|
|
def.onChanged?.(this.mainCtx, next, prevValue)
|
|
this.notifyChanged(key, next)
|
|
}
|
|
}
|
|
}
|
|
|
|
getRaw(key: string): string {
|
|
return this.cache.get(key) ?? ''
|
|
}
|
|
|
|
async setRaw(key: string, value: string): Promise<void> {
|
|
await this.initialize()
|
|
const prev = this.cache.get(key) ?? ''
|
|
if (key in this.definitions) {
|
|
const typedKey = key as settingsKey
|
|
const def = this.definitions[typedKey]
|
|
const nextDeserialized = this.deserializeValue(typedKey, value)
|
|
const nextNormalized = def.normalize ? def.normalize(nextDeserialized) : nextDeserialized
|
|
const nextRaw =
|
|
def.validate && !def.validate(nextNormalized)
|
|
? this.serializeValue(typedKey, def.defaultValue)
|
|
: this.serializeValue(typedKey, nextNormalized)
|
|
|
|
await this.ctx.db.dataSource.getRepository(SettingEntity).save({ key, value: nextRaw })
|
|
this.cache.set(key, nextRaw)
|
|
|
|
const nextTyped = this.parseValue(typedKey, nextRaw)
|
|
const prevTyped = this.parseValue(typedKey, prev)
|
|
if (nextTyped !== prevTyped) {
|
|
def.onChanged?.(this.mainCtx, nextTyped, prevTyped)
|
|
this.notifyChanged(typedKey, nextTyped)
|
|
}
|
|
return
|
|
}
|
|
|
|
await this.ctx.db.dataSource.getRepository(SettingEntity).save({ key, value })
|
|
this.cache.set(key, value)
|
|
}
|
|
|
|
getValue<K extends settingsKey>(key: K): settingsSpec[K] {
|
|
return this.parseValue(key, this.cache.get(key)) as settingsSpec[K]
|
|
}
|
|
|
|
async setValue<K extends settingsKey>(key: K, value: settingsSpec[K]): Promise<void> {
|
|
await this.initialize()
|
|
const def = this.definitions[key]
|
|
const prev = this.getValue(key)
|
|
const normalized = def.normalize ? def.normalize(value) : value
|
|
if (def.validate && !def.validate(normalized)) {
|
|
throw new Error(`Invalid value for setting: ${String(key)}`)
|
|
}
|
|
const raw = this.serializeValue(key, normalized)
|
|
await this.ctx.db.dataSource.getRepository(SettingEntity).save({ key, value: raw })
|
|
this.cache.set(key, raw)
|
|
const next = this.getValue(key)
|
|
if (next !== prev) {
|
|
def.onChanged?.(this.mainCtx, next, prev)
|
|
this.notifyChanged(key, next)
|
|
}
|
|
}
|
|
|
|
getAllRaw(): Record<string, string> {
|
|
const out: Record<string, string> = {}
|
|
for (const [k, v] of this.cache.entries()) out[k] = v
|
|
return out
|
|
}
|
|
|
|
getAll(): settingsSpec {
|
|
const out: any = {}
|
|
for (const key of Object.keys(this.definitions) as settingsKey[]) {
|
|
out[key] = this.getValue(key)
|
|
}
|
|
return out as settingsSpec
|
|
}
|
|
}
|