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 };