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:
Ryan Moon
2026-03-30 06:52:27 -05:00
parent 1f9297f533
commit e346e072b8
10 changed files with 294 additions and 13 deletions

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'
import { api } from '@/lib/api-client'
import { useAuthStore } from '@/stores/auth.store'
import { myPermissionsOptions } from '@/api/rbac'
import { moduleListOptions } from '@/api/modules'
import { Avatar } from '@/components/shared/avatar-upload'
import { Button } from '@/components/ui/button'
import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList, FolderOpen, KeyRound, Settings } from 'lucide-react'
@@ -90,6 +91,17 @@ function AuthenticatedLayout() {
enabled: !!useAuthStore.getState().token,
})
// Fetch enabled modules
const { data: modulesData } = useQuery({
...moduleListOptions(),
enabled: !!useAuthStore.getState().token,
})
const enabledModules = new Set(
(modulesData?.data ?? []).filter((m) => m.enabled && m.licensed).map((m) => m.slug),
)
const isModuleEnabled = (slug: string) => enabledModules.has(slug)
useEffect(() => {
if (permData?.permissions) {
setPermissions(permData.permissions)
@@ -124,7 +136,7 @@ function AuthenticatedLayout() {
<NavLink to="/members" icon={<UserRound className="h-4 w-4" />} label="Members" />
</>
)}
{canViewRepairs && (
{isModuleEnabled('repairs') && canViewRepairs && (
<>
<NavLink to="/repairs" icon={<Wrench className="h-4 w-4" />} label="Repairs" />
<NavLink to="/repair-batches" icon={<Package className="h-4 w-4" />} label="Repair Batches" />
@@ -133,8 +145,12 @@ function AuthenticatedLayout() {
)}
</>
)}
<NavLink to="/files" icon={<FolderOpen className="h-4 w-4" />} label="Files" />
<NavLink to="/vault" icon={<KeyRound className="h-4 w-4" />} label="Vault" />
{isModuleEnabled('files') && (
<NavLink to="/files" icon={<FolderOpen className="h-4 w-4" />} label="Files" />
)}
{isModuleEnabled('vault') && (
<NavLink to="/vault" icon={<KeyRound className="h-4 w-4" />} label="Vault" />
)}
{canViewUsers && (
<div className="mt-4 mb-1 px-3">
<span className="text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wide">Admin</span>