Fix jwt-token
This commit is contained in:
200
server/models/ManagementUser.js
Normal file
200
server/models/ManagementUser.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -27,6 +27,7 @@ const User = require('./User')(sequelize);
|
|||||||
const AlertRule = require('./AlertRule')(sequelize);
|
const AlertRule = require('./AlertRule')(sequelize);
|
||||||
const AlertLog = require('./AlertLog')(sequelize);
|
const AlertLog = require('./AlertLog')(sequelize);
|
||||||
const Tenant = require('./Tenant')(sequelize);
|
const Tenant = require('./Tenant')(sequelize);
|
||||||
|
const ManagementUser = require('./ManagementUser')(sequelize);
|
||||||
|
|
||||||
// Define associations
|
// Define associations
|
||||||
Device.hasMany(DroneDetection, { foreignKey: 'device_id', as: 'detections' });
|
Device.hasMany(DroneDetection, { foreignKey: 'device_id', as: 'detections' });
|
||||||
@@ -56,5 +57,6 @@ module.exports = {
|
|||||||
User,
|
User,
|
||||||
AlertRule,
|
AlertRule,
|
||||||
AlertLog,
|
AlertLog,
|
||||||
Tenant
|
Tenant,
|
||||||
|
ManagementUser
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const router = express.Router();
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const bcrypt = require('bcryptjs'); // Fixed: use bcryptjs instead of bcrypt
|
const bcrypt = require('bcryptjs'); // Fixed: use bcryptjs instead of bcrypt
|
||||||
const { Op } = require('sequelize'); // Add Sequelize operators
|
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
|
// Management-specific authentication middleware - NO shared auth with tenants
|
||||||
const requireManagementAuth = (req, res, next) => {
|
const requireManagementAuth = (req, res, next) => {
|
||||||
@@ -56,20 +56,10 @@ router.post('/auth/login', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
// Hardcoded management users for now (should be in separate DB table)
|
// Use ManagementUser model instead of hardcoded users
|
||||||
const MANAGEMENT_USERS = {
|
const managementUser = await ManagementUser.findByCredentials(username, password);
|
||||||
'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'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const managementUser = MANAGEMENT_USERS[username];
|
if (!managementUser) {
|
||||||
if (!managementUser || !await bcrypt.compare(password, managementUser.password)) {
|
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Invalid management credentials'
|
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 MANAGEMENT_SECRET = process.env.MANAGEMENT_JWT_SECRET || 'mgmt-super-secret-change-in-production';
|
||||||
const token = jwt.sign({
|
const token = jwt.sign({
|
||||||
userId: username,
|
userId: managementUser.id,
|
||||||
username: username,
|
username: managementUser.username,
|
||||||
role: managementUser.role,
|
role: managementUser.role,
|
||||||
isManagement: true
|
isManagement: true
|
||||||
}, MANAGEMENT_SECRET, { expiresIn: '8h' });
|
}, MANAGEMENT_SECRET, { expiresIn: '8h' });
|
||||||
@@ -88,7 +78,11 @@ router.post('/auth/login', async (req, res) => {
|
|||||||
success: true,
|
success: true,
|
||||||
token,
|
token,
|
||||||
user: {
|
user: {
|
||||||
username,
|
id: managementUser.id,
|
||||||
|
username: managementUser.username,
|
||||||
|
email: managementUser.email,
|
||||||
|
first_name: managementUser.first_name,
|
||||||
|
last_name: managementUser.last_name,
|
||||||
role: managementUser.role
|
role: managementUser.role
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -506,14 +500,14 @@ router.post('/tenants/:tenantId/users', async (req, res) => {
|
|||||||
// Create user with tenant association
|
// Create user with tenant association
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
...userData,
|
...userData,
|
||||||
password: hashedPassword,
|
password_hash: hashedPassword, // Use correct field name
|
||||||
tenant_id: tenantId,
|
tenant_id: tenantId,
|
||||||
created_by: req.managementUser.username
|
created_by: req.managementUser.username
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove password from response
|
// Remove password_hash from response
|
||||||
const userResponse = user.toJSON();
|
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}`);
|
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
|
// Hash password if provided
|
||||||
if (updates.password) {
|
if (updates.password) {
|
||||||
const bcrypt = require('bcryptjs');
|
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);
|
await user.update(updates);
|
||||||
|
|
||||||
// Remove password from response
|
// Remove password_hash from response
|
||||||
const userResponse = user.toJSON();
|
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}`);
|
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({
|
const users = await User.findAndCountAll({
|
||||||
where: whereClause,
|
where: whereClause,
|
||||||
attributes: { exclude: ['password'] },
|
attributes: { exclude: ['password_hash'] }, // Exclude password_hash, not password
|
||||||
limit: Math.min(parseInt(limit), 100),
|
limit: Math.min(parseInt(limit), 100),
|
||||||
offset: parseInt(offset),
|
offset: parseInt(offset),
|
||||||
order: [['created_at', 'DESC']]
|
order: [['created_at', 'DESC']]
|
||||||
|
|||||||
63
server/scripts/seed-management-users.js
Normal file
63
server/scripts/seed-management-users.js
Normal file
@@ -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 };
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
const bcrypt = require('bcryptjs');
|
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() {
|
async function seedDatabase() {
|
||||||
try {
|
try {
|
||||||
console.log('🌱 Seeding database...');
|
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' } });
|
const existingAdmin = await User.findOne({ where: { username: 'admin' } });
|
||||||
|
|
||||||
if (!existingAdmin) {
|
if (!existingAdmin) {
|
||||||
|
|||||||
Reference in New Issue
Block a user