diff --git a/server/models/ManagementUser.js b/server/models/ManagementUser.js new file mode 100644 index 0000000..f3d2d61 --- /dev/null +++ b/server/models/ManagementUser.js @@ -0,0 +1,200 @@ +/** + * Management User Model + * Completely separate from tenant users for security isolation + */ + +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const ManagementUser = sequelize.define('ManagementUser', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + username: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + len: [3, 50] + }, + comment: 'Unique management username' + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true + }, + comment: 'Management user email' + }, + password_hash: { + type: DataTypes.STRING, + allowNull: false, + comment: 'Bcrypt hashed password' + }, + first_name: { + type: DataTypes.STRING, + allowNull: true + }, + last_name: { + type: DataTypes.STRING, + allowNull: true + }, + role: { + type: DataTypes.ENUM('super_admin', 'platform_admin', 'support_admin'), + allowNull: false, + defaultValue: 'support_admin', + comment: 'Management role - separate from tenant roles' + }, + is_active: { + type: DataTypes.BOOLEAN, + defaultValue: true, + comment: 'Whether the management user is active' + }, + last_login: { + type: DataTypes.DATE, + allowNull: true, + comment: 'Last successful login timestamp' + }, + login_attempts: { + type: DataTypes.INTEGER, + defaultValue: 0, + comment: 'Failed login attempt counter' + }, + locked_until: { + type: DataTypes.DATE, + allowNull: true, + comment: 'Account lock expiration time' + }, + two_factor_enabled: { + type: DataTypes.BOOLEAN, + defaultValue: false, + comment: 'Whether 2FA is enabled' + }, + two_factor_secret: { + type: DataTypes.STRING, + allowNull: true, + comment: 'TOTP secret for 2FA' + }, + api_access: { + type: DataTypes.BOOLEAN, + defaultValue: true, + comment: 'Whether user can access management API' + }, + permissions: { + type: DataTypes.JSONB, + defaultValue: [], + comment: 'Additional granular permissions' + }, + created_by: { + type: DataTypes.STRING, + allowNull: true, + comment: 'Username of who created this management user' + }, + notes: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Admin notes about this user' + } + }, { + tableName: 'management_users', + indexes: [ + { + unique: true, + fields: ['username'] + }, + { + unique: true, + fields: ['email'] + }, + { + fields: ['role'] + }, + { + fields: ['is_active'] + }, + { + fields: ['last_login'] + } + ], + hooks: { + beforeCreate: (user) => { + // Ensure management users have strong password requirements + // This would be handled in the route, but we can add validation here + }, + beforeUpdate: (user) => { + if (user.changed('password_hash')) { + // Log password changes for security audit + console.log(`Management password change for user: ${user.username}`); + } + } + } + }); + + // Static methods for management user operations + ManagementUser.createInitialAdmin = async function(adminData) { + const bcrypt = require('bcryptjs'); + const hashedPassword = await bcrypt.hash(adminData.password, 12); // Higher cost for management users + + return await this.create({ + username: adminData.username, + email: adminData.email, + password_hash: hashedPassword, + first_name: adminData.first_name, + last_name: adminData.last_name, + role: 'super_admin', + created_by: 'system' + }); + }; + + ManagementUser.findByCredentials = async function(username, password) { + const bcrypt = require('bcryptjs'); + + const user = await this.findOne({ + where: { + username: username, + is_active: true + } + }); + + if (!user) { + return null; + } + + // Check if account is locked + if (user.locked_until && user.locked_until > new Date()) { + throw new Error('Account is temporarily locked'); + } + + const isValidPassword = await bcrypt.compare(password, user.password_hash); + + if (!isValidPassword) { + // Increment failed attempts + await user.increment('login_attempts'); + + // Lock account after 5 failed attempts for 30 minutes + if (user.login_attempts >= 4) { + await user.update({ + locked_until: new Date(Date.now() + 30 * 60 * 1000) // 30 minutes + }); + throw new Error('Account locked due to multiple failed attempts'); + } + + return null; + } + + // Reset login attempts on successful login + await user.update({ + login_attempts: 0, + locked_until: null, + last_login: new Date() + }); + + return user; + }; + + return ManagementUser; +}; diff --git a/server/models/index.js b/server/models/index.js index 501dfe2..2ac4296 100644 --- a/server/models/index.js +++ b/server/models/index.js @@ -27,6 +27,7 @@ const User = require('./User')(sequelize); const AlertRule = require('./AlertRule')(sequelize); const AlertLog = require('./AlertLog')(sequelize); const Tenant = require('./Tenant')(sequelize); +const ManagementUser = require('./ManagementUser')(sequelize); // Define associations Device.hasMany(DroneDetection, { foreignKey: 'device_id', as: 'detections' }); @@ -56,5 +57,6 @@ module.exports = { User, AlertRule, AlertLog, - Tenant + Tenant, + ManagementUser }; diff --git a/server/routes/management.js b/server/routes/management.js index 55ff51f..8f71ff3 100644 --- a/server/routes/management.js +++ b/server/routes/management.js @@ -8,7 +8,7 @@ const router = express.Router(); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); // Fixed: use bcryptjs instead of bcrypt const { Op } = require('sequelize'); // Add Sequelize operators -const { Tenant, User } = require('../models'); +const { Tenant, User, ManagementUser } = require('../models'); // Management-specific authentication middleware - NO shared auth with tenants const requireManagementAuth = (req, res, next) => { @@ -56,20 +56,10 @@ router.post('/auth/login', async (req, res) => { try { const { username, password } = req.body; - // Hardcoded management users for now (should be in separate DB table) - const MANAGEMENT_USERS = { - 'admin': { - password: await bcrypt.hash('admin123', 10), // Change this! - role: 'super_admin' - }, - 'platform_admin': { - password: await bcrypt.hash('platform123', 10), // Change this! - role: 'platform_admin' - } - }; + // Use ManagementUser model instead of hardcoded users + const managementUser = await ManagementUser.findByCredentials(username, password); - const managementUser = MANAGEMENT_USERS[username]; - if (!managementUser || !await bcrypt.compare(password, managementUser.password)) { + if (!managementUser) { return res.status(401).json({ success: false, message: 'Invalid management credentials' @@ -78,8 +68,8 @@ router.post('/auth/login', async (req, res) => { const MANAGEMENT_SECRET = process.env.MANAGEMENT_JWT_SECRET || 'mgmt-super-secret-change-in-production'; const token = jwt.sign({ - userId: username, - username: username, + userId: managementUser.id, + username: managementUser.username, role: managementUser.role, isManagement: true }, MANAGEMENT_SECRET, { expiresIn: '8h' }); @@ -88,7 +78,11 @@ router.post('/auth/login', async (req, res) => { success: true, token, user: { - username, + id: managementUser.id, + username: managementUser.username, + email: managementUser.email, + first_name: managementUser.first_name, + last_name: managementUser.last_name, role: managementUser.role } }); @@ -506,14 +500,14 @@ router.post('/tenants/:tenantId/users', async (req, res) => { // Create user with tenant association const user = await User.create({ ...userData, - password: hashedPassword, + password_hash: hashedPassword, // Use correct field name tenant_id: tenantId, created_by: req.managementUser.username }); - // Remove password from response + // Remove password_hash from response const userResponse = user.toJSON(); - delete userResponse.password; + delete userResponse.password_hash; console.log(`Management: Admin ${req.managementUser.username} created user ${userData.username} in tenant ${tenant.name}`); @@ -563,14 +557,15 @@ router.put('/tenants/:tenantId/users/:userId', async (req, res) => { // Hash password if provided if (updates.password) { const bcrypt = require('bcryptjs'); - updates.password = await bcrypt.hash(updates.password, 10); + updates.password_hash = await bcrypt.hash(updates.password, 10); + delete updates.password; // Remove plain password } await user.update(updates); - // Remove password from response + // Remove password_hash from response const userResponse = user.toJSON(); - delete userResponse.password; + delete userResponse.password_hash; console.log(`Management: Admin ${req.managementUser.username} updated user ${user.username} in tenant ${user.tenant.name}`); @@ -686,7 +681,7 @@ router.get('/tenants/:tenantId/users', async (req, res) => { const users = await User.findAndCountAll({ where: whereClause, - attributes: { exclude: ['password'] }, + attributes: { exclude: ['password_hash'] }, // Exclude password_hash, not password limit: Math.min(parseInt(limit), 100), offset: parseInt(offset), order: [['created_at', 'DESC']] diff --git a/server/scripts/seed-management-users.js b/server/scripts/seed-management-users.js new file mode 100644 index 0000000..c54b884 --- /dev/null +++ b/server/scripts/seed-management-users.js @@ -0,0 +1,63 @@ +/** + * Seed script for creating initial management users + * Run this once to set up the platform admin account + */ + +const bcrypt = require('bcryptjs'); +const { ManagementUser } = require('../models'); + +async function createInitialManagementUser() { + try { + // Check if any management users exist + const existingUsers = await ManagementUser.count(); + + if (existingUsers > 0) { + console.log('✅ Management users already exist. Skipping seed.'); + return; + } + + console.log('🌱 Creating initial management user...'); + + // Hash the password + const hashedPassword = await bcrypt.hash('admin123', 10); + + // Create the admin user + const adminUser = await ManagementUser.create({ + username: 'admin', + email: 'admin@platform.local', + password_hash: hashedPassword, + role: 'super_admin', + first_name: 'Platform', + last_name: 'Administrator', + is_active: true, + created_by: 'system' + }); + + console.log('✅ Initial management user created successfully:'); + console.log(` Username: ${adminUser.username}`); + console.log(` Email: ${adminUser.email}`); + console.log(` Role: ${adminUser.role}`); + console.log(` Password: admin123 (Please change this immediately!)`); + console.log(''); + console.log('🔐 SECURITY WARNING: Change the default password immediately!'); + + } catch (error) { + console.error('❌ Error creating initial management user:', error); + throw error; + } +} + +// If called directly, run the seed +if (require.main === module) { + createInitialManagementUser() + .then(() => { + console.log('🎉 Management user seed completed successfully'); + process.exit(0); + }) + .catch((error) => { + console.error('💥 Management user seed failed:', error); + process.exit(1); + }); +} + +module.exports = { createInitialManagementUser }; diff --git a/server/seedDatabase.js b/server/seedDatabase.js index db8ad99..15dc1d5 100644 --- a/server/seedDatabase.js +++ b/server/seedDatabase.js @@ -1,11 +1,15 @@ const bcrypt = require('bcryptjs'); -const { User, Device, AlertRule } = require('./models'); +const { User, Device, AlertRule, ManagementUser } = require('./models'); +const { createInitialManagementUser } = require('./scripts/seed-management-users'); async function seedDatabase() { try { console.log('🌱 Seeding database...'); - // Check if admin user exists + // First, create management users (platform admins) + await createInitialManagementUser(); + + // Check if admin user exists (legacy tenant admin) const existingAdmin = await User.findOne({ where: { username: 'admin' } }); if (!existingAdmin) {