Add super admin user management
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m46s

This commit is contained in:
Cauê Faleiros
2026-06-16 15:46:04 -03:00
parent f811d3c0ab
commit c8d475808b
12 changed files with 1126 additions and 34 deletions

View File

@@ -0,0 +1,208 @@
const crypto = require('crypto');
const { pool } = require('../db');
const HASH_ALGORITHM = 'scrypt';
const KEY_LENGTH = 64;
const normalizeEmail = (email) => String(email || '').trim().toLowerCase();
const generatePassword = () => {
return crypto.randomBytes(9).toString('base64url');
};
const hashPassword = (password) => {
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.scryptSync(password, salt, KEY_LENGTH).toString('hex');
return `${HASH_ALGORITHM}:${salt}:${hash}`;
};
const verifyPassword = (password, passwordHash) => {
const [algorithm, salt, storedHash] = String(passwordHash || '').split(':');
if (algorithm !== HASH_ALGORITHM || !salt || !storedHash) return false;
const hash = crypto.scryptSync(password, salt, KEY_LENGTH);
const storedBuffer = Buffer.from(storedHash, 'hex');
if (storedBuffer.length !== hash.length) return false;
return crypto.timingSafeEqual(hash, storedBuffer);
};
const publicUserFields = (row) => ({
id: row.id,
name: row.name,
email: row.email,
isActive: row.is_active,
createdAt: row.created_at,
updatedAt: row.updated_at
});
const findUserByEmail = async (email) => {
const normalizedEmail = normalizeEmail(email);
const result = await pool.query(
`SELECT id, name, email, password_hash, is_active, created_at, updated_at
FROM app_users
WHERE LOWER(email) = $1
LIMIT 1`,
[normalizedEmail]
);
return result.rows[0] || null;
};
const listUsers = async () => {
const result = await pool.query(
`SELECT id, name, email, is_active, created_at, updated_at
FROM app_users
ORDER BY created_at DESC, id DESC`
);
return result.rows.map(publicUserFields);
};
const createUser = async ({ name, email, password }) => {
const normalizedName = String(name || '').trim();
const normalizedEmail = normalizeEmail(email);
const generatedPassword = !password;
const plainPassword = String(password || generatePassword()).trim();
if (!normalizedName) {
const error = new Error('Name is required');
error.statusCode = 400;
throw error;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
const error = new Error('Valid email is required');
error.statusCode = 400;
throw error;
}
if (plainPassword.length < 6) {
const error = new Error('Password must have at least 6 characters');
error.statusCode = 400;
throw error;
}
const passwordHash = hashPassword(plainPassword);
try {
const result = await pool.query(
`INSERT INTO app_users (name, email, password_hash)
VALUES ($1, $2, $3)
RETURNING id, name, email, is_active, created_at, updated_at`,
[normalizedName, normalizedEmail, passwordHash]
);
return {
user: publicUserFields(result.rows[0]),
password: plainPassword,
generatedPassword
};
} catch (error) {
if (error.code === '23505') {
const duplicateError = new Error('A user with this email already exists');
duplicateError.statusCode = 409;
throw duplicateError;
}
throw error;
}
};
const findUserById = async (id) => {
const result = await pool.query(
`SELECT id, name, email, password_hash, is_active, created_at, updated_at
FROM app_users
WHERE id = $1
LIMIT 1`,
[id]
);
return result.rows[0] || null;
};
const updateUser = async (id, { name, email, password, isActive }) => {
const existingUser = await findUserById(id);
if (!existingUser) {
const error = new Error('User not found');
error.statusCode = 404;
throw error;
}
const normalizedName = String(name ?? existingUser.name).trim();
const normalizedEmail = normalizeEmail(email ?? existingUser.email);
const normalizedIsActive = typeof isActive === 'boolean' ? isActive : existingUser.is_active;
const normalizedPassword = typeof password === 'string' ? password.trim() : '';
if (!normalizedName) {
const error = new Error('Name is required');
error.statusCode = 400;
throw error;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
const error = new Error('Valid email is required');
error.statusCode = 400;
throw error;
}
if (normalizedPassword && normalizedPassword.length < 6) {
const error = new Error('Password must have at least 6 characters');
error.statusCode = 400;
throw error;
}
const passwordHash = normalizedPassword ? hashPassword(normalizedPassword) : existingUser.password_hash;
try {
const result = await pool.query(
`UPDATE app_users
SET name = $1,
email = $2,
password_hash = $3,
is_active = $4,
updated_at = CURRENT_TIMESTAMP
WHERE id = $5
RETURNING id, name, email, is_active, created_at, updated_at`,
[normalizedName, normalizedEmail, passwordHash, normalizedIsActive, id]
);
return publicUserFields(result.rows[0]);
} catch (error) {
if (error.code === '23505') {
const duplicateError = new Error('A user with this email already exists');
duplicateError.statusCode = 409;
throw duplicateError;
}
throw error;
}
};
const deleteUser = async (id) => {
const result = await pool.query(
`DELETE FROM app_users
WHERE id = $1
RETURNING id`,
[id]
);
if (!result.rowCount) {
const error = new Error('User not found');
error.statusCode = 404;
throw error;
}
};
module.exports = {
createUser,
deleteUser,
findUserById,
findUserByEmail,
generatePassword,
listUsers,
normalizeEmail,
publicUserFields,
updateUser,
verifyPassword
};