docs: Complete Trinity Console 2.0 code package from Gemini
Production-ready code for: Task #95 - Core Engine: - modules/registry.js (module storage) - modules/loader.js (fs discovery + topological sort) - database/migrations.js (per-module migration runner) - modules/routes.js (dynamic route mounting) - events/index.js (EventEmitter registry) - navigation/index.js (permission-filtered nav builder) - boot.js (orchestrates full startup sequence) Task #96 - RBAC Engine: - 001_rbac_tables.sql (5 tables + indexes + seed data) - permissions/resolver.js (override → role → deny logic) - permissions/middleware.js (requirePermission) - permissions/sync.js (upsert from module.json) - permissions/routes.js (API endpoints) Task #98 - Feature Flags: - 002_feature_flags.sql - features/index.js (in-memory cache) - features/middleware.js (requireFeature) - features/routes.js (toggle API) Example Dashboard module with all components. This is the foundation of Trinity Console 2.0. Signed-off-by: Claude (Chronicler #61) <claude@firefrostgaming.com>
This commit is contained in:
@@ -0,0 +1,848 @@
|
|||||||
|
# Gemini Response: Trinity Console 2.0 — Complete Code Package
|
||||||
|
|
||||||
|
**Date:** April 5, 2026
|
||||||
|
**From:** Gemini (Architectural Partner)
|
||||||
|
**To:** Michael (The Wizard) + Claude (Chronicler #61)
|
||||||
|
**Re:** Complete, production-ready code for Tasks #95, #96, #98
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the master blueprint, Michael and Chronicler #61. Let's lay the foundation for Trinity Console 2.0.
|
||||||
|
|
||||||
|
Here is the complete, production-ready code exactly as requested, formatted for easy copy-pasting into your Arbiter environment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Database Migrations (Core Foundation)
|
||||||
|
|
||||||
|
### `migrations/001_rbac_tables.sql`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 1. Roles Table
|
||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. Permissions Table (Updated with is_active)
|
||||||
|
CREATE TABLE IF NOT EXISTS permissions (
|
||||||
|
permission_key VARCHAR(100) PRIMARY KEY,
|
||||||
|
module_id VARCHAR(50) NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. Role Permissions Table
|
||||||
|
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||||
|
role_id INT REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
|
permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (role_id, permission_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 4. User Roles Table
|
||||||
|
CREATE TABLE IF NOT EXISTS user_roles (
|
||||||
|
user_id VARCHAR(50) PRIMARY KEY,
|
||||||
|
role_id INT REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
|
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
assigned_by VARCHAR(50)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 5. User Permission Overrides Table
|
||||||
|
CREATE TABLE IF NOT EXISTS user_permission_overrides (
|
||||||
|
user_id VARCHAR(50) NOT NULL,
|
||||||
|
permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE,
|
||||||
|
is_granted BOOLEAN NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by VARCHAR(50),
|
||||||
|
PRIMARY KEY (user_id, permission_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Core System Migrations Table
|
||||||
|
CREATE TABLE IF NOT EXISTS core_migrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
module_id VARCHAR(50) NOT NULL,
|
||||||
|
migration_file VARCHAR(255) NOT NULL,
|
||||||
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(module_id, migration_file)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_overrides ON user_permission_overrides(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_role_permissions ON role_permissions(role_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_active_permissions ON permissions(is_active);
|
||||||
|
|
||||||
|
-- Seed Data
|
||||||
|
INSERT INTO roles (name, description) VALUES
|
||||||
|
('Admin', 'Full system access (The Wizard)'),
|
||||||
|
('Community', 'Player management and support (The Emissary)'),
|
||||||
|
('Builder', 'Server and infrastructure management (The Catalyst)')
|
||||||
|
ON CONFLICT (name) DO NOTHING;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `migrations/002_feature_flags.sql`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS feature_flags (
|
||||||
|
key VARCHAR(100) PRIMARY KEY,
|
||||||
|
is_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
description TEXT,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_by VARCHAR(50)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Task #95: The Core Engine
|
||||||
|
|
||||||
|
### `src/core/modules/registry.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const modules = new Map();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
register: (moduleConfig) => {
|
||||||
|
modules.set(moduleConfig.id, moduleConfig);
|
||||||
|
},
|
||||||
|
getModule: (id) => modules.get(id),
|
||||||
|
getAllModules: () => Array.from(modules.values()),
|
||||||
|
isModuleEnabled: (id) => modules.has(id) // Can be expanded later if dynamic enable/disable is added
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/modules/loader.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function loadModules(modulesPath) {
|
||||||
|
if (!fs.existsSync(modulesPath)) return [];
|
||||||
|
|
||||||
|
const folders = fs.readdirSync(modulesPath, { withFileTypes: true })
|
||||||
|
.filter(dirent => dirent.isDirectory())
|
||||||
|
.map(dirent => dirent.name);
|
||||||
|
|
||||||
|
const loadedModules = [];
|
||||||
|
|
||||||
|
for (const folder of folders) {
|
||||||
|
const configPath = path.join(modulesPath, folder, 'module.json');
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
config.dirPath = path.join(modulesPath, folder);
|
||||||
|
loadedModules.push(config);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Core] Failed to parse module.json for ${folder}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return topologicalSort(loadedModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
function topologicalSort(modules) {
|
||||||
|
const sorted = [];
|
||||||
|
const visited = new Set();
|
||||||
|
const visiting = new Set();
|
||||||
|
const moduleMap = new Map(modules.map(m => [m.id, m]));
|
||||||
|
|
||||||
|
function visit(moduleId) {
|
||||||
|
if (visiting.has(moduleId)) {
|
||||||
|
throw new Error(`[Core] Fatal: Circular dependency detected involving module '${moduleId}'`);
|
||||||
|
}
|
||||||
|
if (visited.has(moduleId)) return;
|
||||||
|
|
||||||
|
visiting.add(moduleId);
|
||||||
|
const mod = moduleMap.get(moduleId);
|
||||||
|
|
||||||
|
if (mod && mod.dependencies) {
|
||||||
|
for (const dep of Object.keys(mod.dependencies)) {
|
||||||
|
if (!moduleMap.has(dep)) {
|
||||||
|
console.warn(`[Core] Warning: Module '${moduleId}' depends on missing module '${dep}'`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visit(dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visiting.delete(moduleId);
|
||||||
|
visited.add(moduleId);
|
||||||
|
if (mod) sorted.push(mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mod of modules) {
|
||||||
|
visit(mod.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadModules };
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/database/migrations.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
async function runModuleMigrations(pool, moduleConfig) {
|
||||||
|
const migrationsDir = path.join(moduleConfig.dirPath, 'migrations');
|
||||||
|
if (!fs.existsSync(migrationsDir)) return;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(migrationsDir)
|
||||||
|
.filter(f => f.endsWith('.sql'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const checkRes = await pool.query(
|
||||||
|
'SELECT 1 FROM core_migrations WHERE module_id = $1 AND migration_file = $2',
|
||||||
|
[moduleConfig.id, file]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (checkRes.rowCount === 0) {
|
||||||
|
console.log(`[Migrations] Applying ${moduleConfig.id}/${file}...`);
|
||||||
|
const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
await client.query(sql);
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO core_migrations (module_id, migration_file) VALUES ($1, $2)',
|
||||||
|
[moduleConfig.id, file]
|
||||||
|
);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw new Error(`Migration failed: ${moduleConfig.id}/${file} - ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runModuleMigrations };
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/modules/routes.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
function mountModuleRoutes(app, moduleConfig) {
|
||||||
|
const routesPath = path.join(moduleConfig.dirPath, 'routes.js');
|
||||||
|
if (fs.existsSync(routesPath)) {
|
||||||
|
const router = require(routesPath);
|
||||||
|
const prefix = moduleConfig.routes || `/${moduleConfig.id}`;
|
||||||
|
app.use(prefix, router);
|
||||||
|
console.log(`[Routes] Mounted ${moduleConfig.id} at ${prefix}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { mountModuleRoutes };
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/events/index.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class CoreEvents extends EventEmitter {}
|
||||||
|
const coreEmitter = new CoreEvents();
|
||||||
|
|
||||||
|
function loadModuleEvents(moduleConfig) {
|
||||||
|
const eventsPath = path.join(moduleConfig.dirPath, 'events.js');
|
||||||
|
if (fs.existsSync(eventsPath)) {
|
||||||
|
const registerEvents = require(eventsPath);
|
||||||
|
registerEvents(coreEmitter);
|
||||||
|
console.log(`[Events] Registered events for ${moduleConfig.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
emitter: coreEmitter,
|
||||||
|
loadModuleEvents
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/navigation/index.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const registry = require('../modules/registry');
|
||||||
|
|
||||||
|
function buildNavigation(userPermissions) {
|
||||||
|
const modules = registry.getAllModules();
|
||||||
|
const navStructure = {};
|
||||||
|
|
||||||
|
modules.forEach(mod => {
|
||||||
|
if (!mod.nav) return;
|
||||||
|
|
||||||
|
// Basic view permission check based on module ID (assumes {id}.view convention)
|
||||||
|
const canView = userPermissions.includes(`${mod.id}.view`) || userPermissions.includes(`${mod.id}.*`);
|
||||||
|
// If they have admin bypass or specific view permission
|
||||||
|
if (!canView && !userPermissions.includes('admin.*')) return;
|
||||||
|
|
||||||
|
const section = mod.nav.section || 'General';
|
||||||
|
if (!navStructure[section]) navStructure[section] = [];
|
||||||
|
|
||||||
|
navStructure[section].push({
|
||||||
|
name: mod.name,
|
||||||
|
icon: mod.icon || 'folder',
|
||||||
|
path: mod.routes || `/${mod.id}`,
|
||||||
|
position: mod.nav.position || 99,
|
||||||
|
badge: mod.nav.badge // Frontend can interpret this string later
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort items within sections
|
||||||
|
for (const section in navStructure) {
|
||||||
|
navStructure[section].sort((a, b) => a.position - b.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return navStructure;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { buildNavigation };
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/boot.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const path = require('path');
|
||||||
|
const { loadModules } = require('./modules/loader');
|
||||||
|
const registry = require('./modules/registry');
|
||||||
|
const { runModuleMigrations } = require('./database/migrations');
|
||||||
|
const { syncPermissions } = require('./permissions/sync');
|
||||||
|
const { mountModuleRoutes } = require('./modules/routes');
|
||||||
|
const { loadModuleEvents } = require('./events');
|
||||||
|
const { refreshCache } = require('./features');
|
||||||
|
|
||||||
|
async function bootSystem(app, pool) {
|
||||||
|
console.log('[Boot] Initializing Trinity Console 2.0...');
|
||||||
|
const modulesPath = path.join(__dirname, '../../modules');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Load and Sort Modules
|
||||||
|
const sortedModules = loadModules(modulesPath);
|
||||||
|
console.log(`[Boot] Found ${sortedModules.length} valid modules.`);
|
||||||
|
|
||||||
|
for (const mod of sortedModules) {
|
||||||
|
// 2. Register in Memory
|
||||||
|
registry.register(mod);
|
||||||
|
|
||||||
|
// 3. Run Migrations
|
||||||
|
await runModuleMigrations(pool, mod);
|
||||||
|
|
||||||
|
// 4. Register Events
|
||||||
|
loadModuleEvents(mod);
|
||||||
|
|
||||||
|
// 5. Mount Routes
|
||||||
|
mountModuleRoutes(app, mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Sync RBAC Permissions
|
||||||
|
await syncPermissions(pool, sortedModules);
|
||||||
|
|
||||||
|
// 7. Load Feature Flags
|
||||||
|
await refreshCache(pool);
|
||||||
|
|
||||||
|
console.log('[Boot] System fully initialized and ready.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Boot] FATAL ERROR during boot sequence:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { bootSystem };
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Task #96: The RBAC Engine
|
||||||
|
|
||||||
|
### `src/core/permissions/resolver.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function fetchUserPermissions(pool, userId) {
|
||||||
|
const query = `
|
||||||
|
SELECT p.permission_key
|
||||||
|
FROM role_permissions p
|
||||||
|
JOIN user_roles ur ON p.role_id = ur.role_id
|
||||||
|
WHERE ur.user_id = $1
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT permission_key
|
||||||
|
FROM user_permission_overrides
|
||||||
|
WHERE user_id = $1 AND is_granted = TRUE
|
||||||
|
|
||||||
|
EXCEPT
|
||||||
|
|
||||||
|
SELECT permission_key
|
||||||
|
FROM user_permission_overrides
|
||||||
|
WHERE user_id = $1 AND is_granted = FALSE;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { rows } = await pool.query(query, [userId]);
|
||||||
|
return rows.map(r => r.permission_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPermission(userPermissionsArray, requiredPermission) {
|
||||||
|
if (!userPermissionsArray || !Array.isArray(userPermissionsArray)) return false;
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (userPermissionsArray.includes(requiredPermission)) return true;
|
||||||
|
|
||||||
|
// Wildcard match (e.g., required: 'tasks.edit', user has: 'tasks.*')
|
||||||
|
const parts = requiredPermission.split('.');
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const wildcard = `${parts[0]}.*`;
|
||||||
|
if (userPermissionsArray.includes(wildcard)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Super admin wildcard fallback
|
||||||
|
if (userPermissionsArray.includes('admin.*') || userPermissionsArray.includes('*.*')) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { fetchUserPermissions, hasPermission };
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/permissions/middleware.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { hasPermission, fetchUserPermissions } = require('./resolver');
|
||||||
|
const db = require('../../db'); // Adjust path to your db pool connection
|
||||||
|
|
||||||
|
function requirePermission(requiredKey) {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return res.status(401).send('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache resolution in session
|
||||||
|
if (!req.session.permissions) {
|
||||||
|
req.session.permissions = await fetchUserPermissions(db, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPermission(req.session.permissions, requiredKey)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle denial gracefully based on request type
|
||||||
|
const isHtmx = req.headers['hx-request'] === 'true';
|
||||||
|
const msg = `Access Denied: Requires '${requiredKey}' permission.`;
|
||||||
|
|
||||||
|
if (isHtmx) {
|
||||||
|
return res.status(403).send(`
|
||||||
|
<div class="p-4 bg-red-900 border border-red-500 text-white rounded">
|
||||||
|
<strong>Error:</strong> ${msg}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
return res.status(403).render('error', { message: msg }); // Adjust to your error template
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { requirePermission };
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/permissions/sync.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function syncPermissions(pool, modules) {
|
||||||
|
console.log('[RBAC] Syncing permissions from module.json files...');
|
||||||
|
const allDeclaredPermissions = [];
|
||||||
|
|
||||||
|
modules.forEach(mod => {
|
||||||
|
if (mod.permissions && Array.isArray(mod.permissions)) {
|
||||||
|
mod.permissions.forEach(p => {
|
||||||
|
allDeclaredPermissions.push({
|
||||||
|
key: p.key,
|
||||||
|
moduleId: mod.id,
|
||||||
|
name: p.name,
|
||||||
|
description: p.description
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// 1. Mark everything as inactive first
|
||||||
|
await client.query('UPDATE permissions SET is_active = FALSE');
|
||||||
|
|
||||||
|
// 2. Upsert declared permissions and mark active
|
||||||
|
for (const p of allDeclaredPermissions) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO permissions (permission_key, module_id, name, description, is_active)
|
||||||
|
VALUES ($1, $2, $3, $4, TRUE)
|
||||||
|
ON CONFLICT (permission_key) DO UPDATE
|
||||||
|
SET name = EXCLUDED.name,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
is_active = TRUE;
|
||||||
|
`, [p.key, p.moduleId, p.name, p.description]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
console.log(`[RBAC] Synced ${allDeclaredPermissions.length} permissions successfully.`);
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
console.error('[RBAC] Failed to sync permissions:', err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { syncPermissions };
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/permissions/routes.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require('../../db');
|
||||||
|
const { requirePermission } = require('./middleware');
|
||||||
|
|
||||||
|
// Protect all RBAC routes
|
||||||
|
router.use(requirePermission('system.rbac.manage'));
|
||||||
|
|
||||||
|
router.get('/api/permissions', async (req, res) => {
|
||||||
|
const { rows } = await db.query('SELECT * FROM permissions WHERE is_active = TRUE ORDER BY module_id, name');
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/api/roles', async (req, res) => {
|
||||||
|
const { rows } = await db.query('SELECT * FROM roles ORDER BY id');
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/api/users/:id/role', async (req, res) => {
|
||||||
|
const { role_id } = req.body;
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO user_roles (user_id, role_id, assigned_by) VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (user_id) DO UPDATE SET role_id = EXCLUDED.role_id, assigned_by = EXCLUDED.assigned_by`,
|
||||||
|
[req.params.id, role_id, req.user.id]
|
||||||
|
);
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/api/users/:id/override', async (req, res) => {
|
||||||
|
const { permission_key, is_granted } = req.body;
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO user_permission_overrides (user_id, permission_key, is_granted, created_by)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (user_id, permission_key) DO UPDATE
|
||||||
|
SET is_granted = EXCLUDED.is_granted, created_by = EXCLUDED.created_by`,
|
||||||
|
[req.params.id, permission_key, is_granted, req.user.id]
|
||||||
|
);
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/api/users/:id/override/:key', async (req, res) => {
|
||||||
|
await db.query(
|
||||||
|
'DELETE FROM user_permission_overrides WHERE user_id = $1 AND permission_key = $2',
|
||||||
|
[req.params.id, req.params.key]
|
||||||
|
);
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Task #98: Feature Flags
|
||||||
|
|
||||||
|
### `src/core/features/index.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let featureCache = {};
|
||||||
|
|
||||||
|
async function refreshCache(pool) {
|
||||||
|
try {
|
||||||
|
const { rows } = await pool.query('SELECT key, is_enabled FROM feature_flags');
|
||||||
|
const newCache = {};
|
||||||
|
rows.forEach(r => { newCache[r.key] = r.is_enabled; });
|
||||||
|
featureCache = newCache;
|
||||||
|
console.log('[Features] Feature flag cache refreshed.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Features] Failed to refresh cache:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnabled(key) {
|
||||||
|
return !!featureCache[key]; // Default to false if missing
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setFlag(pool, key, enabled, updatedBy) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO feature_flags (key, is_enabled, updated_by) VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (key) DO UPDATE SET is_enabled = EXCLUDED.is_enabled, updated_by = EXCLUDED.updated_by`,
|
||||||
|
[key, enabled, updatedBy]
|
||||||
|
);
|
||||||
|
await refreshCache(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { refreshCache, isEnabled, setFlag };
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/features/middleware.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { isEnabled } = require('./index');
|
||||||
|
|
||||||
|
function requireFeature(featureKey) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
if (isEnabled(featureKey)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHtmx = req.headers['hx-request'] === 'true';
|
||||||
|
const msg = `Feature '${featureKey}' is currently disabled.`;
|
||||||
|
|
||||||
|
if (isHtmx) {
|
||||||
|
return res.status(503).send(`
|
||||||
|
<div class="p-4 bg-yellow-900 border border-yellow-500 text-white rounded">
|
||||||
|
<strong>Service Unavailable:</strong> ${msg}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
return res.status(503).send(msg); // Adjust to your 503 template
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { requireFeature };
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/core/features/routes.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require('../../db');
|
||||||
|
const { requirePermission } = require('../permissions/middleware');
|
||||||
|
const { setFlag } = require('./index');
|
||||||
|
|
||||||
|
router.use(requirePermission('system.features.manage'));
|
||||||
|
|
||||||
|
router.get('/api/features', async (req, res) => {
|
||||||
|
const { rows } = await db.query('SELECT * FROM feature_flags ORDER BY key');
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/api/features/:key', async (req, res) => {
|
||||||
|
const { is_enabled } = req.body;
|
||||||
|
await setFlag(db, req.params.key, is_enabled, req.user.id);
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Boot Integration (`src/index.js`)
|
||||||
|
|
||||||
|
### `src/index.js` *(Snippet showing integration with existing Express setup)*
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const session = require('express-session');
|
||||||
|
const pgSession = require('connect-pg-simple')(session);
|
||||||
|
const db = require('./db'); // Your existing PG pool
|
||||||
|
const { bootSystem } = require('./core/boot');
|
||||||
|
const featureRoutes = require('./core/features/routes');
|
||||||
|
const rbacRoutes = require('./core/permissions/routes');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Existing Middlewares (Body parser, Session, Passport, etc.)
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
// ... session setup ...
|
||||||
|
|
||||||
|
// Initialize Trinity Core Architecture
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await bootSystem(app, db);
|
||||||
|
|
||||||
|
// Mount core API routes
|
||||||
|
app.use('/core', featureRoutes);
|
||||||
|
app.use('/core', rbacRoutes);
|
||||||
|
|
||||||
|
// Start listening only after boot is complete
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`[Arbiter] Trinity Console 2.0 running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Arbiter] FAILED TO START:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Complete Example Module (Dashboard)
|
||||||
|
|
||||||
|
### `src/modules/dashboard/module.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "dashboard",
|
||||||
|
"name": "Dashboard",
|
||||||
|
"description": "System overview and quick stats",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Trinity",
|
||||||
|
"dependencies": {},
|
||||||
|
"icon": "home",
|
||||||
|
"nav": {
|
||||||
|
"section": "General",
|
||||||
|
"position": 1
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"key": "dashboard.view",
|
||||||
|
"name": "View Dashboard",
|
||||||
|
"description": "Access the main overview screen"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routes": "/dashboard"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/modules/dashboard/routes.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { requirePermission } = require('../../core/permissions/middleware');
|
||||||
|
|
||||||
|
// Protect entire module
|
||||||
|
router.use(requirePermission('dashboard.view'));
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
// res.render path defaults relative to your app's view engine setup.
|
||||||
|
// You may need to adjust the path based on your exact EJS setup.
|
||||||
|
res.render('../src/modules/dashboard/views/index', { user: req.user });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/modules/dashboard/views/index.ejs`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="p-6 bg-slate-900 text-white min-h-screen">
|
||||||
|
<h1 class="text-3xl font-bold mb-4 text-blue-400">Welcome back, <%= user.username %></h1>
|
||||||
|
<p class="text-slate-300">The Frostwall holds strong.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
|
||||||
|
<div class="bg-slate-800 p-4 border border-slate-700 rounded-lg">
|
||||||
|
<h2 class="text-xl font-bold">Servers Online</h2>
|
||||||
|
<p class="text-3xl text-emerald-400 mt-2">12 / 12</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/modules/dashboard/events.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = function(coreEvents) {
|
||||||
|
coreEvents.on('server.status_change', (data) => {
|
||||||
|
console.log(`[Dashboard Module] Noticed server ${data.serverId} changed to ${data.status}`);
|
||||||
|
// Could update a cache here or emit over WebSockets to UI
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/modules/dashboard/api.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Functions exposed for other modules to use via registry.getModule('dashboard').api
|
||||||
|
module.exports = {
|
||||||
|
getSystemHealthScore: async () => {
|
||||||
|
return 100; // Placeholder logic
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/modules/dashboard/migrations/001_dashboard_stats.sql`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Placeholder migration to prove the engine works
|
||||||
|
CREATE TABLE IF NOT EXISTS dashboard_daily_stats (
|
||||||
|
date DATE PRIMARY KEY,
|
||||||
|
total_logins INT DEFAULT 0
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure Summary
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/arbiter-3.0/
|
||||||
|
├── migrations/
|
||||||
|
│ ├── 001_rbac_tables.sql
|
||||||
|
│ └── 002_feature_flags.sql
|
||||||
|
├── src/
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── boot.js
|
||||||
|
│ │ ├── database/
|
||||||
|
│ │ │ └── migrations.js
|
||||||
|
│ │ ├── events/
|
||||||
|
│ │ │ └── index.js
|
||||||
|
│ │ ├── features/
|
||||||
|
│ │ │ ├── index.js
|
||||||
|
│ │ │ ├── middleware.js
|
||||||
|
│ │ │ └── routes.js
|
||||||
|
│ │ ├── modules/
|
||||||
|
│ │ │ ├── loader.js
|
||||||
|
│ │ │ ├── registry.js
|
||||||
|
│ │ │ └── routes.js
|
||||||
|
│ │ ├── navigation/
|
||||||
|
│ │ │ └── index.js
|
||||||
|
│ │ └── permissions/
|
||||||
|
│ │ ├── middleware.js
|
||||||
|
│ │ ├── resolver.js
|
||||||
|
│ │ ├── routes.js
|
||||||
|
│ │ └── sync.js
|
||||||
|
│ ├── modules/
|
||||||
|
│ │ └── dashboard/
|
||||||
|
│ │ ├── module.json
|
||||||
|
│ │ ├── routes.js
|
||||||
|
│ │ ├── views/
|
||||||
|
│ │ │ └── index.ejs
|
||||||
|
│ │ ├── events.js
|
||||||
|
│ │ ├── migrations/
|
||||||
|
│ │ │ └── 001_dashboard_stats.sql
|
||||||
|
│ │ └── api.js
|
||||||
|
│ ├── db.js (existing)
|
||||||
|
│ └── index.js (updated)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Response received April 5, 2026*
|
||||||
Reference in New Issue
Block a user