201 lines
4.9 KiB
JavaScript
201 lines
4.9 KiB
JavaScript
/**
|
|
* 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: sequelize.Sequelize.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;
|
|
};
|