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

@@ -13,7 +13,9 @@ import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { AvatarUpload } from '@/components/shared/avatar-upload'
import { Save, Plus, Trash2, MapPin, Building, ImageIcon } from 'lucide-react'
import { Switch } from '@/components/ui/switch'
import { moduleListOptions, moduleMutations, moduleKeys, type ModuleConfig } from '@/api/modules'
import { Save, Plus, Trash2, MapPin, Building, ImageIcon, Blocks, Lock } from 'lucide-react'
import { toast } from 'sonner'
interface StoreSettings {
@@ -232,10 +234,69 @@ function SettingsPage() {
)}
</CardContent>
</Card>
{/* Modules */}
<ModulesCard />
</div>
)
}
function ModulesCard() {
const queryClient = useQueryClient()
const hasPermission = useAuthStore((s) => s.hasPermission)
const canEdit = hasPermission('settings.edit')
const { data: modulesData, isLoading } = useQuery(moduleListOptions())
const modules = modulesData?.data ?? []
const toggleMutation = useMutation({
mutationFn: ({ slug, enabled }: { slug: string; enabled: boolean }) => moduleMutations.toggle(slug, enabled),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: moduleKeys.list })
toast.success('Module updated')
},
onError: (err) => toast.error(err.message),
})
return (
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Blocks className="h-5 w-5" />Modules
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-3">{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-12 w-full" />)}</div>
) : (
<div className="space-y-2">
{modules.map((mod) => (
<div key={mod.id} className="flex items-center justify-between p-3 rounded-md border">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{mod.name}</span>
{!mod.licensed && (
<Badge variant="outline" className="text-[10px] gap-1">
<Lock className="h-2.5 w-2.5" />Not Licensed
</Badge>
)}
</div>
{mod.description && <p className="text-xs text-muted-foreground mt-0.5">{mod.description}</p>}
</div>
<Switch
checked={mod.enabled}
onCheckedChange={(checked) => toggleMutation.mutate({ slug: mod.slug, enabled: checked })}
disabled={!canEdit || !mod.licensed || toggleMutation.isPending}
/>
</div>
))}
</div>
)}
</CardContent>
</Card>
)
}
function LocationCard({ location }: { location: Location }) {
const queryClient = useQueryClient()
const [editing, setEditing] = useState(false)