- SQLite database with full schema: users, roles, permissions, role_permissions, user_roles, services, audit_log - RBAC engine with wildcard permission resolution (*.*.*) - Automatic v2→v3 migration from JSON files - 5 default roles: super_admin, admin, editor, user, viewer - Feature registration for APP, GGL, FDX (119 permissions) - 8 services seeded - Full API: roles CRUD, permission check, user-role assignment, feature registration, audit log, stats - Backward compatible with existing auth flows
238 lines
11 KiB
JavaScript
238 lines
11 KiB
JavaScript
const db = require('./db');
|
|
const rbac = require('./rbac');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const bcrypt = require('bcryptjs');
|
|
|
|
const DATA_DIR = path.join(__dirname, '..', 'data');
|
|
const USERS_FILE = path.join(DATA_DIR, 'users.json');
|
|
|
|
function migrate() {
|
|
console.log('[migration] Starting v2 → v3 migration...');
|
|
|
|
// 1. Migrate users from JSON
|
|
migrateUsers();
|
|
|
|
// 2. Seed default roles
|
|
seedRoles();
|
|
|
|
// 3. Seed wildcard permissions
|
|
seedWildcardPermissions();
|
|
|
|
// 4. Assign roles to migrated users
|
|
assignMigratedUserRoles();
|
|
|
|
// 5. Register initial features for UseCaseGen apps
|
|
registerUseCaseGenFeatures();
|
|
|
|
// 6. Seed services
|
|
seedServices();
|
|
|
|
console.log('[migration] Migration complete.');
|
|
}
|
|
|
|
function migrateUsers() {
|
|
if (!fs.existsSync(USERS_FILE)) {
|
|
console.log('[migration] No users.json found — skipping user migration');
|
|
return;
|
|
}
|
|
|
|
const existing = db.prepare('SELECT COUNT(*) as c FROM users').get();
|
|
if (existing.c > 0) {
|
|
console.log(`[migration] Users table already has ${existing.c} rows — skipping`);
|
|
return;
|
|
}
|
|
|
|
const users = JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
|
|
const insert = db.prepare(`
|
|
INSERT OR IGNORE INTO users (email, name, password_hash, auth_methods, metadata, tags, created_at, created_by, last_login, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
let count = 0;
|
|
for (const u of users) {
|
|
insert.run(
|
|
u.email, u.name, u.passwordHash || null,
|
|
JSON.stringify(u.authMethods || ['google']),
|
|
JSON.stringify(u.metadata || {}),
|
|
JSON.stringify(u.tags || []),
|
|
u.createdAt || new Date().toISOString(),
|
|
u.createdBy || 'system',
|
|
u.lastLogin || null,
|
|
'active'
|
|
);
|
|
count++;
|
|
}
|
|
console.log(`[migration] Migrated ${count} users from JSON`);
|
|
}
|
|
|
|
function seedRoles() {
|
|
const existing = db.prepare('SELECT COUNT(*) as c FROM roles').get();
|
|
if (existing.c > 0) {
|
|
console.log(`[migration] Roles already seeded (${existing.c}) — skipping`);
|
|
return;
|
|
}
|
|
|
|
const roles = [
|
|
['super_admin', 'Super Administrator', 'Full access to all apps and features', 1, 1],
|
|
['admin', 'Administrator', 'App-level admin with full feature access', 1, 10],
|
|
['editor', 'Editor', 'Can create, edit, and manage content', 1, 20],
|
|
['user', 'User', 'Standard user — chat and content reading', 1, 30],
|
|
['viewer', 'Viewer', 'Read-only access', 1, 40],
|
|
];
|
|
|
|
const insert = db.prepare('INSERT INTO roles (name, display_name, description, is_system, priority) VALUES (?, ?, ?, ?, ?)');
|
|
for (const r of roles) insert.run(...r);
|
|
console.log(`[migration] Seeded ${roles.length} default roles`);
|
|
}
|
|
|
|
function seedWildcardPermissions() {
|
|
const existing = db.prepare("SELECT COUNT(*) as c FROM permissions WHERE app = '*'").get();
|
|
if (existing.c > 0) return;
|
|
|
|
// Wildcard permission entries
|
|
const wildcards = [
|
|
['*', '*', '*', 'Full Access', 'Unrestricted access to everything', 'system'],
|
|
['*', '*', 'read', 'Global Read', 'Read access to all features', 'system'],
|
|
['*', '*', 'write', 'Global Write', 'Write access to all features', 'system'],
|
|
['*', '*', 'delete', 'Global Delete', 'Delete access to all features', 'system'],
|
|
['*', '*', 'admin', 'Global Admin', 'Admin access to all features', 'system'],
|
|
['*', 'chat', 'read', 'Chat Read (all apps)', 'Read chat in all apps', 'chat'],
|
|
['*', 'chat', 'write', 'Chat Write (all apps)', 'Write chat in all apps', 'chat'],
|
|
['*', 'content', 'read', 'Content Read (all apps)', 'Read content in all apps', 'content'],
|
|
['*', 'content', 'write', 'Content Write (all apps)', 'Write content in all apps', 'content'],
|
|
['*', 'content', 'delete', 'Content Delete (all apps)', 'Delete content in all apps', 'content'],
|
|
];
|
|
|
|
const insert = db.prepare('INSERT OR IGNORE INTO permissions (app, feature, action, display_name, description, category) VALUES (?, ?, ?, ?, ?, ?)');
|
|
for (const w of wildcards) insert.run(...w);
|
|
|
|
// Assign permissions to roles
|
|
const superAdmin = db.prepare("SELECT id FROM roles WHERE name = 'super_admin'").get();
|
|
const admin = db.prepare("SELECT id FROM roles WHERE name = 'admin'").get();
|
|
const editor = db.prepare("SELECT id FROM roles WHERE name = 'editor'").get();
|
|
const user = db.prepare("SELECT id FROM roles WHERE name = 'user'").get();
|
|
const viewer = db.prepare("SELECT id FROM roles WHERE name = 'viewer'").get();
|
|
|
|
const perm = (app, feat, act) => db.prepare('SELECT id FROM permissions WHERE app=? AND feature=? AND action=?').get(app, feat, act);
|
|
|
|
const assign = db.prepare('INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)');
|
|
|
|
// super_admin: *.*.*
|
|
assign.run(superAdmin.id, perm('*', '*', '*').id);
|
|
|
|
// admin: *.*.read, *.*.write, *.*.delete, *.*.admin
|
|
assign.run(admin.id, perm('*', '*', 'read').id);
|
|
assign.run(admin.id, perm('*', '*', 'write').id);
|
|
assign.run(admin.id, perm('*', '*', 'delete').id);
|
|
assign.run(admin.id, perm('*', '*', 'admin').id);
|
|
|
|
// editor: *.content.*, *.chat.*
|
|
assign.run(editor.id, perm('*', 'content', 'read').id);
|
|
assign.run(editor.id, perm('*', 'content', 'write').id);
|
|
assign.run(editor.id, perm('*', 'content', 'delete').id);
|
|
assign.run(editor.id, perm('*', 'chat', 'read').id);
|
|
assign.run(editor.id, perm('*', 'chat', 'write').id);
|
|
|
|
// user: *.chat.read, *.chat.write, *.content.read
|
|
assign.run(user.id, perm('*', 'chat', 'read').id);
|
|
assign.run(user.id, perm('*', 'chat', 'write').id);
|
|
assign.run(user.id, perm('*', 'content', 'read').id);
|
|
|
|
// viewer: *.*.read
|
|
assign.run(viewer.id, perm('*', '*', 'read').id);
|
|
|
|
console.log('[migration] Seeded wildcard permissions and role assignments');
|
|
}
|
|
|
|
function assignMigratedUserRoles() {
|
|
const existingAssignments = db.prepare('SELECT COUNT(*) as c FROM user_roles').get();
|
|
if (existingAssignments.c > 0) {
|
|
console.log(`[migration] User-role assignments already exist (${existingAssignments.c}) — skipping`);
|
|
return;
|
|
}
|
|
|
|
// Read original users.json to get their v2 role + services
|
|
if (!fs.existsSync(USERS_FILE)) return;
|
|
const v2Users = JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
|
|
|
|
for (const v2 of v2Users) {
|
|
const user = db.prepare('SELECT id FROM users WHERE email = ? COLLATE NOCASE').get(v2.email);
|
|
if (!user) continue;
|
|
|
|
if (v2.role === 'admin' && v2.services && v2.services.includes('*')) {
|
|
// Admin with wildcard → super_admin
|
|
rbac.assignRole(v2.email, 'super_admin', '*', 'migration');
|
|
console.log(`[migration] ${v2.email} → super_admin (global)`);
|
|
} else if (v2.role === 'admin') {
|
|
// Admin with specific services → admin scoped
|
|
for (const svc of (v2.services || [])) {
|
|
rbac.assignRole(v2.email, 'admin', svc, 'migration');
|
|
}
|
|
console.log(`[migration] ${v2.email} → admin (scoped: ${(v2.services || []).join(', ')})`);
|
|
} else {
|
|
// Everyone else → user
|
|
rbac.assignRole(v2.email, 'user', '*', 'migration');
|
|
console.log(`[migration] ${v2.email} → user (global)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function registerUseCaseGenFeatures() {
|
|
const existingPerms = db.prepare("SELECT COUNT(*) as c FROM permissions WHERE app = 'usecasegen'").get();
|
|
if (existingPerms.c > 0) return;
|
|
|
|
const commonFeatures = [
|
|
{ feature: 'chat', actions: ['read', 'write'], category: 'chat', display_name: 'Chat / Sessions' },
|
|
{ feature: 'manage.focus_areas', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Focus Areas' },
|
|
{ feature: 'manage.tech_patterns', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Tech Patterns' },
|
|
{ feature: 'manage.regions', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Regions' },
|
|
{ feature: 'manage.audience', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Audience' },
|
|
{ feature: 'manage.analysis_focus', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Analysis Focus' },
|
|
{ feature: 'sessions', actions: ['read', 'write', 'delete'], category: 'content', display_name: 'Saved Sessions' },
|
|
{ feature: 'admin_panel', actions: ['read', 'admin'], category: 'admin', display_name: 'Admin Panel' },
|
|
{ feature: 'user_management', actions: ['read', 'write', 'admin'], category: 'admin', display_name: 'User Management' },
|
|
{ feature: 'settings', actions: ['read', 'write'], category: 'settings', display_name: 'App Settings' },
|
|
];
|
|
|
|
rbac.registerFeatures('usecasegen', commonFeatures);
|
|
rbac.registerFeatures('ggl-usecasegen', commonFeatures);
|
|
|
|
const fdxFeatures = [
|
|
{ feature: 'chat', actions: ['read', 'write'], category: 'chat', display_name: 'Chat / Sessions' },
|
|
{ feature: 'manage.patterns', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Patterns' },
|
|
{ feature: 'manage.regions', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Regions' },
|
|
{ feature: 'manage.audience', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Audience' },
|
|
{ feature: 'manage.analysis', actions: ['read', 'write', 'delete', 'hide', 'archive'], category: 'content', display_name: 'Analysis Focus' },
|
|
{ feature: 'sessions', actions: ['read', 'write', 'delete'], category: 'content', display_name: 'Saved Sessions' },
|
|
{ feature: 'enterprise_systems', actions: ['read', 'write', 'delete'], category: 'content', display_name: 'Enterprise Systems' },
|
|
{ feature: 'admin_panel', actions: ['read', 'admin'], category: 'admin', display_name: 'Admin Panel' },
|
|
{ feature: 'user_management', actions: ['read', 'write', 'admin'], category: 'admin', display_name: 'User Management' },
|
|
{ feature: 'settings', actions: ['read', 'write'], category: 'settings', display_name: 'App Settings' },
|
|
];
|
|
|
|
rbac.registerFeatures('fedex-cohort-app', fdxFeatures);
|
|
console.log('[migration] Registered UseCaseGen features for all three apps');
|
|
}
|
|
|
|
function seedServices() {
|
|
const existing = db.prepare('SELECT COUNT(*) as c FROM services').get();
|
|
if (existing.c > 0) return;
|
|
|
|
const services = [
|
|
['usecasegen', 'UseCaseGen APP', 'app.usecasegen.app', '["app.usecasegen.app","usecasegen.app","www.usecasegen.app"]', 3006],
|
|
['ggl-usecasegen', 'GGL UseCaseGen', 'ggl.usecasegen.app', '["ggl.usecasegen.app"]', 3095],
|
|
['fedex-cohort-app', 'FDX Trade Intel', 'fdx.usecasegen.app', '["fdx.usecasegen.app"]', 3089],
|
|
['taos', 'GoalStack / TAOS', 'goalstack.scottfelten.com', '["goalstack.scottfelten.com","taos.scottfelten.com"]', null],
|
|
['access-manager', 'Access Manager', 'users.scottfelten.com', '["users.scottfelten.com"]', 3030],
|
|
['intellicert', 'IntelliCert', 'intellicert.app', '["intellicert.app","www.intellicert.app"]', null],
|
|
['customer-360', 'Customer 360', 'c360.scottfelten.com', '["c360.scottfelten.com"]', null],
|
|
['command-center', 'Docs / Command Center', 'docs.scottfelten.com', '["docs.scottfelten.com"]', null],
|
|
];
|
|
|
|
const insert = db.prepare('INSERT OR IGNORE INTO services (id, display_name, hostname, hostnames, port) VALUES (?, ?, ?, ?, ?)');
|
|
for (const s of services) insert.run(...s);
|
|
console.log(`[migration] Seeded ${services.length} services`);
|
|
}
|
|
|
|
module.exports = { migrate };
|