Fix jwt-token

This commit is contained in:
2025-09-13 13:13:43 +02:00
parent 04b103fe32
commit 0518f8dd5d
5 changed files with 291 additions and 27 deletions

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

View File

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

View File

@@ -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']]

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

View File

@@ -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) {