Add module management system for enabling/disabling features
Stores can enable/disable feature modules from Settings. When disabled, nav links are hidden and API routes return 403. Designed as the foundation for future license-based gating (licensed + enabled flags). Core modules (Accounts, Members, Users, Roles, Settings) are always on. - module_config table with slug, name, description, licensed, enabled - In-memory cache for fast per-request module checks - requireModule middleware wraps route groups in main.ts - Settings page Modules card with toggle switches - Sidebar hides nav links for disabled modules - Default modules seeded: inventory, pos, repairs, rentals, lessons, files, vault, email, reports
This commit is contained in:
51
packages/backend/src/services/module.service.ts
Normal file
51
packages/backend/src/services/module.service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import { moduleConfig } from '../db/schema/modules.js'
|
||||
|
||||
// In-memory cache of enabled module slugs — avoids DB query per request
|
||||
let enabledCache: Set<string> | null = null
|
||||
|
||||
export const ModuleService = {
|
||||
async listAll(db: PostgresJsDatabase<any>) {
|
||||
return db.select().from(moduleConfig).orderBy(moduleConfig.name)
|
||||
},
|
||||
|
||||
async isEnabled(db: PostgresJsDatabase<any>, slug: string): Promise<boolean> {
|
||||
if (!enabledCache) await this.refreshCache(db)
|
||||
return enabledCache!.has(slug)
|
||||
},
|
||||
|
||||
async getEnabledSlugs(db: PostgresJsDatabase<any>): Promise<Set<string>> {
|
||||
if (!enabledCache) await this.refreshCache(db)
|
||||
return new Set(enabledCache!)
|
||||
},
|
||||
|
||||
async setEnabled(db: PostgresJsDatabase<any>, slug: string, enabled: boolean): Promise<typeof moduleConfig.$inferSelect | null> {
|
||||
// Check that module exists and is licensed
|
||||
const [mod] = await db.select().from(moduleConfig).where(eq(moduleConfig.slug, slug)).limit(1)
|
||||
if (!mod) return null
|
||||
if (enabled && !mod.licensed) {
|
||||
throw new Error('Module is not licensed')
|
||||
}
|
||||
|
||||
const [updated] = await db.update(moduleConfig)
|
||||
.set({ enabled, updatedAt: new Date() })
|
||||
.where(eq(moduleConfig.slug, slug))
|
||||
.returning()
|
||||
|
||||
// Invalidate cache
|
||||
enabledCache = null
|
||||
|
||||
return updated ?? null
|
||||
},
|
||||
|
||||
async refreshCache(db: PostgresJsDatabase<any>) {
|
||||
const modules = await db.select({ slug: moduleConfig.slug, enabled: moduleConfig.enabled, licensed: moduleConfig.licensed })
|
||||
.from(moduleConfig)
|
||||
enabledCache = new Set(modules.filter((m) => m.enabled && m.licensed).map((m) => m.slug))
|
||||
},
|
||||
|
||||
invalidateCache() {
|
||||
enabledCache = null
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user