diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 25967e6..554ce5a 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -427,3 +427,23 @@ model Setting { @@map("settings") } + +// ─── Nav Modules ───────────────────────────────────────────────────────────── + +model NavModule { + id Int @id @default(autoincrement()) + name String @unique + label String + sectionId String + sectionLabel String + sectionIcon String @default("Settings") + sectionTo String? + sectionWeight Int @default(100) + weight Int @default(100) + route String + enabled Boolean @default(true) + builtIn Boolean @default(true) + createdAt DateTime @default(now()) + + @@map("nav_modules") +} diff --git a/server/src/db/navSeed.ts b/server/src/db/navSeed.ts new file mode 100644 index 0000000..28c2137 --- /dev/null +++ b/server/src/db/navSeed.ts @@ -0,0 +1,256 @@ +type Seed = { + name: string; label: string; route: string + sectionId: string; sectionLabel: string; sectionIcon: string + sectionTo?: string; sectionWeight: number; weight: number +} + +const s = (sId: string, sLbl: string, icon: string, sw: number, sTo?: string) => + (nm: string, lbl: string, rt: string, w: number): Seed => + ({ name: nm, label: lbl, route: rt, sectionId: sId, sectionLabel: sLbl, sectionIcon: icon, sectionTo: sTo, sectionWeight: sw, weight: w }) + +const sc = s('server-config', 'Server Configuration', 'Settings', 10, '/server-configuration') +const su = s('support', 'Support', 'HelpCircle', 20) +const nw = s('networking', 'Networking Setup', 'Network', 30) +const co = s('contacts', 'Server Contacts', 'BookUser', 40) +const rs = s('resellers', 'Resellers', 'UserCog', 50) +const sv = s('service-config', 'Service Configuration', 'Wrench', 60) +const lc = s('locales', 'Locales', 'Languages', 70) +const bk = s('backup', 'Backup', 'HardDrive', 80) +const cl = s('clusters', 'Clusters', 'GitFork', 90) +const rb = s('reboot', 'System Reboot', 'RotateCcw', 100) +const st = s('server-status', 'Server Status', 'Activity', 110) +const ai = s('account-info', 'Account Information', 'Users', 120) +const af = s('account-functions', 'Account Functions', 'UserCheck', 130) +const ma = s('multi-account', 'Multi Account Functions', 'Users', 140) +const tr = s('transfers', 'Transfers', 'ArrowRightLeft',150) +const th = s('themes', 'Themes', 'Paintbrush', 160) +const pk = s('packages', 'Packages', 'Package', 170) +const dn = s('dns', 'DNS Functions', 'Globe', 180) +const db = s('databases', 'Database Services', 'Database', 190) +const ip = s('ip', 'IP Functions', 'Wifi', 200) +const sw2= s('software', 'Software', 'Box', 210) +const em = s('email', 'Email', 'Mail', 220) +const sh = s('system-health', 'System Health', 'HeartPulse', 230) +const np = s('nixpanel', 'NixPanel', 'LayoutDashboard',240) +const sl = s('ssl', 'SSL/TLS', 'Lock', 250) +const mk = s('market', 'Market', 'ShoppingBag', 260) +const re = s('restart-services', 'Restart Services', 'RefreshCw', 270) +const dv = s('development', 'Development', 'Code2', 280) +const pl = s('plugins', 'Plugins', 'Puzzle', 290) + +export const NAV_SEED: Seed[] = [ + sc('basic-nixserver-setup', 'Basic NixServer Setup', '/server-configuration/basic-setup', 10), + sc('change-root', 'Change Root Password', '/server-configuration/change-root', 20), + sc('configure-analytics', 'Configure Analytics', '/server-configuration/analytics', 30), + sc('configure-cron-jobs', 'Configure Cron Jobs', '/server-configuration/cron-jobs', 40), + sc('initial-quota-setup', 'Initial Quota Setup', '/server-configuration/quota-setup', 50), + sc('link-server-nodes', 'Link Server Nodes', '/server-configuration/link-nodes', 60), + sc('server-profile', 'Server Profile', '/server-configuration/server-profile', 70), + sc('server-time', 'Server Time', '/server-configuration/server-time', 80), + sc('statistics-software', 'Statistics Software', '/server-configuration/statistics-software', 90), + sc('terminal', 'Terminal', '/server-configuration/terminal', 100), + sc('tweak-settings', 'Tweak Settings', '/server-configuration/tweak-settings', 110), + sc('update-preferences', 'Update Preferences', '/server-configuration/update-preferences', 120), + sc('nixserver-marketplace', 'NixServer Marketplace', '/server-configuration/marketplace', 130), + su('grant-support-access', 'Grant NixPanel Support Access', '/security', 10), + su('support-center', 'Support Center', '/settings', 20), + nw('change-hostname', 'Change Hostname', '/security', 10), + nw('resolver-config', 'Resolver Configuration', '/security', 20), + nw('security-center', 'Security Center', '/security', 30), + nw('apache-mod-userdir', 'Apache mod_userdir Tweak', '/webserver', 40), + nw('smtp-restrictions', 'SMTP Restrictions', '/email', 50), + nw('compiler-access', 'Compiler Access', '/security', 60), + nw('security-policies', 'Configure Security Policies', '/security', 70), + nw('mxstop-brute-force', 'mXSTOP Brute Force Protection', '/security', 80), + nw('host-access-control', 'Host Access Control', '/security', 90), + nw('external-auth', 'Manage External Authentications', '/security', 100), + nw('root-ssh-keys', "Manage root's SSH Keys", '/security', 110), + nw('wheel-users', 'Manage Wheel Group Users', '/security', 120), + nw('modsecurity-config', 'ModSecurity Configuration', '/security', 130), + nw('modsecurity-tools', 'ModSecurity Tools', '/security', 140), + nw('modsecurity-vendors', 'ModSecurity Vendors', '/security', 150), + nw('password-strength', 'Password Strength Configuration', '/security', 160), + nw('security-advisor', 'Security Advisor', '/security', 170), + nw('security-questions', 'Security Questions', '/security', 180), + nw('shell-fork-bomb', 'Shell Fork Bomb Protection', '/security', 190), + nw('ssh-password-auth', 'SSH Password Authorization Tweak', '/security', 200), + nw('traceroute', 'Traceroute Enable/Disable', '/security', 210), + nw('two-factor-auth', 'Two-Factor Authentication', '/security', 220), + co('contact-manager', 'Contact Manager', '/settings', 10), + co('system-mail-prefs', 'Edit System Mail Preferences', '/email', 20), + rs('change-account-owner', 'Change Ownership of an Account', '/resellers', 10), + rs('change-multi-owner', 'Change Ownership of Multiple Accounts', '/resellers', 20), + rs('edit-reseller-ns', 'Edit Reseller Nameservers and Privileges', '/resellers', 30), + rs('email-all-resellers', 'Email All Resellers', '/resellers', 40), + rs('reseller-ip-delegation', "Manage Reseller's IP Delegation", '/resellers', 50), + rs('reseller-shared-ip', "Manage Reseller's Shared IP", '/resellers', 60), + rs('reseller-center', 'Reseller Center', '/resellers', 70), + rs('reset-resellers', 'Reset Resellers', '/resellers', 80), + rs('show-reseller-accounts', 'Show Reseller Accounts', '/resellers', 90), + rs('view-reseller-usage', 'View Reseller Usage', '/resellers', 100), + sv('apache-configuration', 'Apache Configuration', '/webserver', 10), + sv('log-rotation', 'Log Rotation Configuration', '/settings', 20), + sv('web-disk-config', 'Web Disk Configuration', '/settings', 30), + sv('web-services-config', 'Web Services Configuration', '/settings', 40), + sv('exim-config', 'Exim Configuration Manager', '/email', 50), + sv('ftp-server-selection', 'FTP Server Selection', '/settings', 60), + sv('mailserver-config', 'Mailserver Configuration', '/email', 70), + sv('manage-service-ssl', 'Manage Service SSL Certificates', '/ssl', 80), + sv('nameserver-selection', 'Nameserver Selection', '/dns', 90), + sv('service-manager', 'Service Manager', '/services', 100), + lc('configure-locales', 'Configure Application Locales', '/settings', 10), + lc('copy-locale', 'Copy a Locale', '/settings', 20), + lc('delete-locale', 'Delete a Locale', '/settings', 30), + lc('edit-locale', 'Edit a Locale', '/settings', 40), + lc('locale-xml-download', 'Locale XML Download', '/settings', 50), + lc('locale-xml-upload', 'Locale XML Upload', '/settings', 60), + lc('view-locales', 'View Available Locales', '/settings', 70), + bk('backup-configuration', 'Backup Configuration', '/backup', 10), + bk('backup-restoration', 'Backup Restoration', '/backup', 20), + bk('backup-user-selection', 'Backup User Selection', '/backup', 30), + bk('file-dir-restoration', 'File and Directory Restoration', '/backup', 40), + cl('config-cluster', 'Configuration Cluster', '/settings', 10), + cl('dns-cluster', 'DNS Cluster', '/dns', 20), + cl('remote-access-key', 'Remote Access Key', '/security', 30), + rb('force-reboot', 'Forceful Server Reboot', '/system', 10), + rb('graceful-reboot', 'Graceful Server Reboot', '/system', 20), + st('apache-status', 'Apache Status', '/webserver', 10), + st('daily-process-log', 'Daily Process Log', '/system', 20), + st('server-information', 'Server Information', '/system', 30), + st('service-status', 'Service Status', '/services', 40), + st('task-queue-monitor', 'Task Queue Monitor', '/system', 50), + ai('list-accounts', 'List Accounts', '/accounts/list', 10), + ai('list-parked-domains', 'List Parked Domains', '/accounts/parked-domains', 20), + ai('list-subdomains', 'List Subdomains', '/accounts/subdomains', 30), + ai('list-suspended', 'List Suspended Accounts', '/accounts/suspended', 40), + ai('show-over-quota', 'Show Accounts Over Quota', '/accounts/over-quota', 50), + ai('view-bandwidth', 'View Bandwidth Usage', '/accounts/bandwidth', 60), + af('change-site-ip', "Change Site's IP Address", '/accounts/change-ip', 10), + af('create-account', 'Create a New Account', '/accounts/create', 20), + af('email-all-users', 'Email All Users', '/accounts/email-users', 30), + af('force-password-change', 'Force Password Change', '/accounts/force-password', 40), + af('limit-bandwidth', 'Limit Bandwidth Usage', '/accounts/limit-bandwidth',50), + af('manage-suspension', 'Manage Account Suspension', '/accounts/suspension', 60), + af('manage-demo-mode', 'Manage Demo Mode', '/accounts/demo-mode', 70), + af('manage-shell-access', 'Manage Shell Access', '/accounts/shell-access', 80), + af('modify-account', 'Modify an Account', '/accounts/modify', 90), + af('password-modification', 'Password Modification', '/accounts/password', 100), + af('quota-modification', 'Quota Modification', '/accounts/quota', 110), + af('raw-apache-log', 'Raw Apache Log Download', '/accounts/apache-logs', 120), + af('raw-nginx-log', 'Raw NGINX Log Download', '/accounts/nginx-logs', 130), + af('rearrange-account', 'Rearrange an Account', '/accounts/rearrange', 140), + af('reset-acct-bandwidth', 'Reset Account Bandwidth', '/accounts/reset-bandwidth',150), + af('terminate-accounts', 'Terminate Accounts', '/accounts/terminate', 160), + af('unsuspend-bw', 'Unsuspend Bandwidth Exceeders', '/accounts/unsuspend-bandwidth',170), + af('upgrade-downgrade', 'Upgrade/Downgrade an Account', '/accounts/upgrade', 180), + af('web-template-editor', 'Web Template Editor', '/accounts/web-template', 190), + ma('bulk-ip', "Change Multiple Sites' IP Addresses",'/accounts/bulk-ip', 10), + ma('bulk-modify', 'Modify/Upgrade Multiple Accounts', '/accounts/bulk-modify', 20), + tr('addon-to-account', 'Convert Addon Domain to Account', '/accounts', 10), + tr('review-transfers', 'Review Transfers and Restores', '/accounts', 20), + tr('transfer-cpanel', 'Transfer or Restore a cPanel Account','/accounts',30), + tr('transfer-tool', 'Transfer Tool', '/accounts', 40), + th('theme-manager', 'Theme Manager', '/settings', 10), + pk('add-package', 'Add a Package', '/packages', 10), + pk('delete-package', 'Delete a Package', '/packages', 20), + pk('edit-package', 'Edit a Package', '/packages', 30), + pk('feature-manager', 'Feature Manager', '/packages', 40), + dn('add-dns-zone', 'Add a DNS Zone', '/dns', 10), + dn('add-a-entry', 'Add an A Entry for Hostname', '/dns', 20), + dn('delete-dns-zone', 'Delete a DNS Zone', '/dns', 30), + dn('dns-zone-manager', 'DNS Zone Manager', '/dns', 40), + dn('edit-zone-templates', 'Edit Zone Templates', '/dns', 50), + dn('email-dns-records', 'Email DNS Record Manager', '/dns', 60), + dn('email-routing-config', 'Email Routing Configuration', '/email', 70), + dn('enable-dkim-spf', 'Enable DKIM/SPF Globally', '/email', 80), + dn('ns-record-report', 'Nameserver Record Report', '/dns', 90), + dn('park-domain', 'Park a Domain', '/dns', 100), + dn('dns-cleanup', 'Perform a DNS Cleanup', '/dns', 110), + dn('set-zone-ttl', 'Set Zone TTL', '/dns', 120), + dn('domain-forwarding', 'Setup/Edit Domain Forwarding', '/dns', 130), + dn('sync-dns-records', 'Synchronize DNS Records', '/dns', 140), + db('change-db-root-pass', 'Change Database Root Password', '/settings', 10), + db('change-db-user-pass', 'Change Database User Password', '/settings', 20), + db('db-map-tool', 'Database Map Tool', '/settings', 30), + db('edit-db-config', 'Edit Database Configuration', '/settings', 40), + db('manage-db-hosts', 'Manage Database Access Hosts', '/settings', 50), + db('manage-db-profiles', 'Manage Database Profiles', '/settings', 60), + db('manage-db-users', 'Manage Database Users', '/settings', 70), + db('manage-databases', 'Manage Databases', '/settings', 80), + db('phpmyadmin', 'phpMyAdmin', '/settings', 90), + db('repair-databases', 'Repair Databases', '/settings', 100), + db('show-db-processes', 'Show Database Processes', '/settings', 110), + db('upgrade-db-version', 'Upgrade Database Version', '/settings', 120), + ip('ipv6-ranges', 'IPv6 Ranges', '/security', 10), + ip('add-ip', 'Add a New IP Address', '/security', 20), + ip('assign-ipv6', 'Assign IPv6 Address', '/security', 30), + ip('configure-remote-ips', 'Configure Remote Service IPs', '/security', 40), + ip('ip-migration', 'IP Migration Wizard', '/security', 50), + ip('rebuild-ip-pool', 'Rebuild the IP Address Pool', '/security', 60), + ip('show-ip-usage', 'Show IP Address Usage', '/security', 70), + ip('show-delete-ips', 'Show or Delete Current IPs', '/security', 80), + ip('reserved-ips', 'Show/Edit Reserved IPs', '/security', 90), + sw2('install-perl-module', 'Install a Perl Module', '/system', 10), + sw2('install-distro-pkgs', 'Install Distro Packages', '/system', 20), + sw2('module-installers', 'Module Installers', '/system', 30), + sw2('multiphp-ini-editor', 'MultiPHP INI Editor', '/php', 40), + sw2('multiphp-manager', 'MultiPHP Manager', '/php', 50), + sw2('nginx-manager', 'NGINX Manager', '/webserver', 60), + sw2('rebuild-rpm-db', 'Rebuild RPM Database', '/system', 70), + sw2('system-update', 'System Update', '/system', 80), + sw2('update-server-software','Update Server Software', '/system', 90), + sw2('easyapache4', 'EasyApache 4', '/webserver', 100), + em('email-deliverability', 'Email Deliverability', '/email', 10), + em('filter-by-country', 'Filter Incoming Emails by Country', '/email', 20), + em('filter-by-domain', 'Filter Incoming Emails by Domain', '/email', 30), + em('greylisting', 'Greylisting', '/email', 40), + em('mail-delivery-reports', 'Mail Delivery Reports', '/email', 50), + em('mail-queue-manager', 'Mail Queue Manager', '/email', 60), + em('mail-troubleshooter', 'Mail Troubleshooter', '/email', 70), + em('mailbox-conversion', 'Mailbox Conversion', '/email', 80), + em('repair-mailbox-perms', 'Repair Mailbox Permissions', '/email', 90), + em('spamd-startup', 'Spamd Startup Configuration', '/email', 100), + em('view-mail-stats', 'View Mail Statistics Summary', '/email', 110), + em('view-relayers', 'View Relayers', '/email', 120), + em('view-sent-summary', 'View Sent Summary', '/email', 130), + sh('bg-process-killer', 'Background Process Killer', '/system', 10), + sh('process-manager', 'Process Manager', '/system', 20), + sh('show-disk-usage', 'Show Current Disk Usage', '/system', 30), + sh('show-processes', 'Show Current Running Processes', '/system', 40), + np('changelog', 'Change Log', '/settings', 10), + np('customization', 'Customization', '/settings', 20), + np('manage-plugins', 'Manage Plugins', '/modules', 30), + np('modify-news', 'Modify NixPanel News', '/settings', 40), + np('reset-mailman-pass', 'Reset a Mailman Password', '/email', 50), + np('upgrade-latest', 'Upgrade to Latest Version', '/settings', 60), + sl('generate-ssl', 'Generate an SSL Certificate and Signing Request', '/ssl', 10), + sl('install-ssl', 'Install an SSL Certificate on a Domain', '/ssl', 20), + sl('manage-autossl', 'Manage AutoSSL', '/ssl', 30), + sl('manage-ssl-hosts', 'Manage SSL Hosts', '/ssl', 40), + sl('purchase-ssl', 'Purchase and Install an SSL Certificate', '/ssl', 50), + sl('ssl-storage', 'SSL Storage Manager', '/ssl', 60), + sl('ssl-config', 'SSL/TLS Configuration', '/ssl', 70), + mk('market-provider', 'Market Provider Manager', '/settings', 10), + re('restart-db', 'Database Server', '/services', 10), + re('restart-dns', 'DNS Server', '/services', 20), + re('restart-apache', 'HTTP Server (Apache)', '/services', 30), + re('restart-imap', 'IMAP Server', '/services', 40), + re('restart-exim', 'Mail Server (Exim)', '/services', 50), + re('restart-mailman', 'Mailing List Manager (Mailman)', '/services', 60), + re('restart-php-fpm', 'PHP-FPM service for Apache', '/services', 70), + re('restart-ssh', 'SSH Server (OpenSSH)', '/services', 80), + dv('appconfig-apps', 'Apps Managed by AppConfig', '/settings', 10), + dv('dev-forum', 'NixPanel Development Forum', '/settings', 20), + dv('plugin-file-gen', 'NixPanel Plugin File Generator', '/settings', 30), + dv('developer-docs', 'Developer Documentation', '/settings', 40), + dv('manage-api-tokens', 'Manage API Tokens', '/settings', 50), + dv('manage-hooks', 'Manage Hooks', '/settings', 60), + dv('openapi-nixpanel', 'OpenAPI Documentation for NixPanel', '/settings', 70), + dv('openapi-nixserver', 'OpenAPI Documentation for NixServer','/settings', 80), + pl('backuply', 'Backuply by Softaculous', '/backup', 10), + pl('imunifyav', 'ImunifyAV', '/security', 20), + pl('server-monitoring', 'Server Monitoring', '/system', 30), + pl('sitepad', 'SitePad Website Builder', '/settings', 40), + pl('softaculous', 'Softaculous - Instant Installs', '/settings', 50), + pl('wp-toolkit', 'WP Toolkit', '/settings', 60), +] diff --git a/server/src/db/seed.ts b/server/src/db/seed.ts index d272896..e66a0c4 100644 --- a/server/src/db/seed.ts +++ b/server/src/db/seed.ts @@ -90,6 +90,22 @@ async function main() { } console.log('Created default settings') + // Nav modules + const { NAV_SEED } = await import('./navSeed.js') + for (const m of NAV_SEED) { + await prisma.navModule.upsert({ + where: { name: m.name }, + update: { + label: m.label, route: m.route, + sectionId: m.sectionId, sectionLabel: m.sectionLabel, + sectionIcon: m.sectionIcon, sectionTo: m.sectionTo ?? null, + sectionWeight: m.sectionWeight, weight: m.weight, + }, + create: { ...m, sectionTo: m.sectionTo ?? null, enabled: true, builtIn: true }, + }) + } + console.log(`Seeded ${NAV_SEED.length} nav modules`) + console.log('Seed complete.') } diff --git a/server/src/routes/nixserver/modules/index.ts b/server/src/routes/nixserver/modules/index.ts new file mode 100644 index 0000000..d53588d --- /dev/null +++ b/server/src/routes/nixserver/modules/index.ts @@ -0,0 +1,106 @@ +import type { FastifyInstance } from 'fastify' +import { z } from 'zod' +import prisma from '../../../db/client.js' +import { requireAdmin } from '../../../middleware/auth.js' + +const ModuleBody = z.object({ + name: z.string().min(1).max(64).regex(/^[a-z0-9-]+$/), + label: z.string().min(1).max(128), + sectionId: z.string().min(1).max(64), + sectionLabel: z.string().min(1).max(128), + sectionIcon: z.string().min(1).max(64).default('Settings'), + sectionTo: z.string().optional(), + sectionWeight: z.number().int().default(100), + weight: z.number().int().default(100), + route: z.string().min(1).max(256), +}) + +const UpdateBody = z.object({ + label: z.string().min(1).max(128).optional(), + sectionId: z.string().min(1).max(64).optional(), + sectionLabel: z.string().min(1).max(128).optional(), + sectionIcon: z.string().min(1).max(64).optional(), + sectionTo: z.string().nullable().optional(), + sectionWeight: z.number().int().optional(), + weight: z.number().int().optional(), + route: z.string().min(1).max(256).optional(), + enabled: z.boolean().optional(), +}) + +export default async function moduleRoutes(fastify: FastifyInstance) { + // ── GET /nav — grouped nav for sidebar (enabled modules only) ────────────── + fastify.get('/nav', { preHandler: [requireAdmin] }, async (_req, reply) => { + const modules = await prisma.navModule.findMany({ + where: { enabled: true }, + orderBy: [{ sectionWeight: 'asc' }, { weight: 'asc' }], + }) + + // Group by sectionId + const sectionMap = new Map() + + for (const m of modules) { + if (!sectionMap.has(m.sectionId)) { + sectionMap.set(m.sectionId, { + id: m.sectionId, label: m.sectionLabel, + icon: m.sectionIcon, to: m.sectionTo, + items: [], + }) + } + sectionMap.get(m.sectionId)!.items.push(m) + } + + return reply.send({ success: true, data: Array.from(sectionMap.values()) }) + }) + + // ── GET / — all modules for Module Manager ───────────────────────────────── + fastify.get('/', { preHandler: [requireAdmin] }, async (_req, reply) => { + const modules = await prisma.navModule.findMany({ + orderBy: [{ sectionWeight: 'asc' }, { weight: 'asc' }], + }) + return reply.send({ success: true, data: modules }) + }) + + // ── POST / — install a new module ────────────────────────────────────────── + fastify.post('/', { preHandler: [requireAdmin] }, async (request, reply) => { + const body = ModuleBody.safeParse(request.body) + if (!body.success) return reply.code(400).send({ success: false, error: body.error.issues[0].message }) + + const existing = await prisma.navModule.findUnique({ where: { name: body.data.name } }) + if (existing) return reply.code(409).send({ success: false, error: 'Module name already exists' }) + + const module = await prisma.navModule.create({ + data: { ...body.data, sectionTo: body.data.sectionTo ?? null, enabled: true, builtIn: false }, + }) + return reply.code(201).send({ success: true, data: module }) + }) + + // ── PUT /:id — update module ─────────────────────────────────────────────── + fastify.put('/:id', { preHandler: [requireAdmin] }, async (request, reply) => { + const id = parseInt((request.params as { id: string }).id) + if (isNaN(id)) return reply.code(400).send({ success: false, error: 'Invalid id' }) + + const body = UpdateBody.safeParse(request.body) + if (!body.success) return reply.code(400).send({ success: false, error: body.error.issues[0].message }) + + const module = await prisma.navModule.findUnique({ where: { id } }) + if (!module) return reply.code(404).send({ success: false, error: 'Module not found' }) + + const updated = await prisma.navModule.update({ where: { id }, data: body.data }) + return reply.send({ success: true, data: updated }) + }) + + // ── DELETE /:id — remove a non-built-in module ───────────────────────────── + fastify.delete('/:id', { preHandler: [requireAdmin] }, async (request, reply) => { + const id = parseInt((request.params as { id: string }).id) + if (isNaN(id)) return reply.code(400).send({ success: false, error: 'Invalid id' }) + + const module = await prisma.navModule.findUnique({ where: { id } }) + if (!module) return reply.code(404).send({ success: false, error: 'Module not found' }) + if (module.builtIn) return reply.code(403).send({ success: false, error: 'Cannot delete a built-in module' }) + + await prisma.navModule.delete({ where: { id } }) + return reply.send({ success: true, data: null }) + }) +}