import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs'; import { join, dirname } from 'path'; import { DEFAULT_THEME, type ThemeConfig } from './theme.defaults'; const THEME_PATH = join(process.cwd(), 'data', 'theme.json'); const BACKUP_DIR = join(process.cwd(), 'data', 'theme-backups'); @Injectable() export class ThemeService implements OnModuleInit { private readonly logger = new Logger(ThemeService.name); private cached: ThemeConfig | null = null; onModuleInit() { this.load(); } getTheme(): ThemeConfig { if (this.cached) return this.cached; return this.load(); } updateTheme(updates: Partial): ThemeConfig { const current = this.getTheme(); const merged: ThemeConfig = { brand: { ...current.brand, ...updates.brand }, colors: { brand: { ...current.colors.brand, ...updates.colors?.brand }, }, typography: { ...current.typography, ...updates.typography }, login: { ...current.login, ...updates.login, poweredBy: { ...current.login.poweredBy, ...updates.login?.poweredBy } }, sidebar: { ...current.sidebar, ...updates.sidebar }, ai: { quickActions: updates.ai?.quickActions ?? current.ai.quickActions, }, }; merged.version = (current.version ?? 0) + 1; merged.updatedAt = new Date().toISOString(); this.backup(); const dir = dirname(THEME_PATH); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); writeFileSync(THEME_PATH, JSON.stringify(merged, null, 2), 'utf8'); this.cached = merged; this.logger.log(`Theme updated to v${merged.version}`); return merged; } resetTheme(): ThemeConfig { this.backup(); const dir = dirname(THEME_PATH); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); writeFileSync(THEME_PATH, JSON.stringify(DEFAULT_THEME, null, 2), 'utf8'); this.cached = DEFAULT_THEME; this.logger.log('Theme reset to defaults'); return DEFAULT_THEME; } private load(): ThemeConfig { try { if (existsSync(THEME_PATH)) { const raw = readFileSync(THEME_PATH, 'utf8'); const parsed = JSON.parse(raw); this.cached = { brand: { ...DEFAULT_THEME.brand, ...parsed.brand }, colors: { brand: { ...DEFAULT_THEME.colors.brand, ...parsed.colors?.brand } }, typography: { ...DEFAULT_THEME.typography, ...parsed.typography }, login: { ...DEFAULT_THEME.login, ...parsed.login, poweredBy: { ...DEFAULT_THEME.login.poweredBy, ...parsed.login?.poweredBy } }, sidebar: { ...DEFAULT_THEME.sidebar, ...parsed.sidebar }, ai: { quickActions: parsed.ai?.quickActions ?? DEFAULT_THEME.ai.quickActions }, }; this.logger.log('Theme loaded from file'); return this.cached; } } catch (err) { this.logger.warn(`Failed to load theme: ${err}`); } this.cached = DEFAULT_THEME; this.logger.log('Using default theme'); return DEFAULT_THEME; } private backup() { try { if (!existsSync(THEME_PATH)) return; if (!existsSync(BACKUP_DIR)) mkdirSync(BACKUP_DIR, { recursive: true }); const ts = new Date().toISOString().replace(/[:.]/g, '-'); copyFileSync(THEME_PATH, join(BACKUP_DIR, `theme-${ts}.json`)); } catch (err) { this.logger.warn(`Backup failed: ${err}`); } } }