mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2026-01-06 13:09:34 -06:00
705 lines
18 KiB
JavaScript
705 lines
18 KiB
JavaScript
const express = require('express');
|
|
const bcrypt = require('bcryptjs');
|
|
const jwt = require('jsonwebtoken');
|
|
const { PrismaClient } = require('@prisma/client');
|
|
const { body, validationResult } = require('express-validator');
|
|
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
|
const { requireViewUsers, requireManageUsers } = require('../middleware/permissions');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
|
|
const router = express.Router();
|
|
const prisma = new PrismaClient();
|
|
|
|
// Generate JWT token
|
|
const generateToken = (userId) => {
|
|
return jwt.sign(
|
|
{ userId },
|
|
process.env.JWT_SECRET || 'your-secret-key',
|
|
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
|
);
|
|
};
|
|
|
|
// Admin endpoint to list all users
|
|
router.get('/admin/users', authenticateToken, requireViewUsers, async (req, res) => {
|
|
try {
|
|
const users = await prisma.users.findMany({
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
role: true,
|
|
isActive: true,
|
|
lastLogin: true,
|
|
createdAt: true,
|
|
updatedAt: true
|
|
},
|
|
orderBy: {
|
|
createdAt: 'desc'
|
|
}
|
|
})
|
|
|
|
res.json(users)
|
|
} catch (error) {
|
|
console.error('List users error:', error)
|
|
res.status(500).json({ error: 'Failed to fetch users' })
|
|
}
|
|
})
|
|
|
|
// Admin endpoint to create a new user
|
|
router.post('/admin/users', authenticateToken, requireManageUsers, [
|
|
body('username').isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
|
|
body('email').isEmail().withMessage('Valid email is required'),
|
|
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
|
|
body('role').optional().custom(async (value) => {
|
|
if (!value) return true; // Optional field
|
|
const rolePermissions = await prisma.role_permissions.findUnique({
|
|
where: { role: value }
|
|
});
|
|
if (!rolePermissions) {
|
|
throw new Error('Invalid role specified');
|
|
}
|
|
return true;
|
|
})
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { username, email, password, role = 'user' } = req.body;
|
|
|
|
// Check if user already exists
|
|
const existingUser = await prisma.users.findFirst({
|
|
where: {
|
|
OR: [
|
|
{ username },
|
|
{ email }
|
|
]
|
|
}
|
|
});
|
|
|
|
if (existingUser) {
|
|
return res.status(409).json({ error: 'Username or email already exists' });
|
|
}
|
|
|
|
// Hash password
|
|
const passwordHash = await bcrypt.hash(password, 12);
|
|
|
|
// Create user
|
|
const user = await prisma.users.create({
|
|
data: {
|
|
username,
|
|
email,
|
|
passwordHash,
|
|
role
|
|
},
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
role: true,
|
|
isActive: true,
|
|
createdAt: true
|
|
}
|
|
});
|
|
|
|
res.status(201).json({
|
|
message: 'User created successfully',
|
|
user
|
|
});
|
|
} catch (error) {
|
|
console.error('User creation error:', error);
|
|
res.status(500).json({ error: 'Failed to create user' });
|
|
}
|
|
});
|
|
|
|
// Admin endpoint to update a user
|
|
router.put('/admin/users/:userId', authenticateToken, requireManageUsers, [
|
|
body('username').optional().isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
|
|
body('email').optional().isEmail().withMessage('Valid email is required'),
|
|
body('role').optional().custom(async (value) => {
|
|
if (!value) return true; // Optional field
|
|
const rolePermissions = await prisma.role_permissions.findUnique({
|
|
where: { role: value }
|
|
});
|
|
if (!rolePermissions) {
|
|
throw new Error('Invalid role specified');
|
|
}
|
|
return true;
|
|
}),
|
|
body('isActive').optional().isBoolean().withMessage('isActive must be a boolean')
|
|
], async (req, res) => {
|
|
try {
|
|
const { userId } = req.params;
|
|
const errors = validationResult(req);
|
|
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { username, email, role, isActive } = req.body;
|
|
const updateData = {};
|
|
|
|
if (username) updateData.username = username;
|
|
if (email) updateData.email = email;
|
|
if (role) updateData.role = role;
|
|
if (typeof isActive === 'boolean') updateData.isActive = isActive;
|
|
|
|
// Check if user exists
|
|
const existingUser = await prisma.users.findUnique({
|
|
where: { id: userId }
|
|
});
|
|
|
|
if (!existingUser) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
|
|
// Check if username/email already exists (excluding current user)
|
|
if (username || email) {
|
|
const duplicateUser = await prisma.users.findFirst({
|
|
where: {
|
|
AND: [
|
|
{ id: { not: userId } },
|
|
{
|
|
OR: [
|
|
...(username ? [{ username }] : []),
|
|
...(email ? [{ email }] : [])
|
|
]
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
if (duplicateUser) {
|
|
return res.status(409).json({ error: 'Username or email already exists' });
|
|
}
|
|
}
|
|
|
|
// Prevent deactivating the last admin
|
|
if (isActive === false && existingUser.role === 'admin') {
|
|
const adminCount = await prisma.users.count({
|
|
where: {
|
|
role: 'admin',
|
|
isActive: true
|
|
}
|
|
});
|
|
|
|
if (adminCount <= 1) {
|
|
return res.status(400).json({ error: 'Cannot deactivate the last admin user' });
|
|
}
|
|
}
|
|
|
|
// Update user
|
|
const updatedUser = await prisma.users.update({
|
|
where: { id: userId },
|
|
data: updateData,
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
role: true,
|
|
isActive: true,
|
|
lastLogin: true,
|
|
createdAt: true,
|
|
updatedAt: true
|
|
}
|
|
});
|
|
|
|
res.json({
|
|
message: 'User updated successfully',
|
|
user: updatedUser
|
|
});
|
|
} catch (error) {
|
|
console.error('User update error:', error);
|
|
res.status(500).json({ error: 'Failed to update user' });
|
|
}
|
|
});
|
|
|
|
// Admin endpoint to delete a user
|
|
router.delete('/admin/users/:userId', authenticateToken, requireManageUsers, async (req, res) => {
|
|
try {
|
|
const { userId } = req.params;
|
|
|
|
// Prevent self-deletion
|
|
if (userId === req.user.id) {
|
|
return res.status(400).json({ error: 'Cannot delete your own account' });
|
|
}
|
|
|
|
// Check if user exists
|
|
const user = await prisma.users.findUnique({
|
|
where: { id: userId }
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
|
|
// Prevent deleting the last admin
|
|
if (user.role === 'admin') {
|
|
const adminCount = await prisma.users.count({
|
|
where: {
|
|
role: 'admin',
|
|
isActive: true
|
|
}
|
|
});
|
|
|
|
if (adminCount <= 1) {
|
|
return res.status(400).json({ error: 'Cannot delete the last admin user' });
|
|
}
|
|
}
|
|
|
|
// Delete user
|
|
await prisma.users.delete({
|
|
where: { id: userId }
|
|
});
|
|
|
|
res.json({
|
|
message: 'User deleted successfully'
|
|
});
|
|
} catch (error) {
|
|
console.error('User deletion error:', error);
|
|
res.status(500).json({ error: 'Failed to delete user' });
|
|
}
|
|
});
|
|
|
|
// Admin endpoint to reset user password
|
|
router.post('/admin/users/:userId/reset-password', authenticateToken, requireManageUsers, [
|
|
body('newPassword').isLength({ min: 6 }).withMessage('New password must be at least 6 characters')
|
|
], async (req, res) => {
|
|
try {
|
|
const { userId } = req.params;
|
|
const errors = validationResult(req);
|
|
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { newPassword } = req.body;
|
|
|
|
// Check if user exists
|
|
const user = await prisma.users.findUnique({
|
|
where: { id: userId },
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
role: true,
|
|
is_active: true
|
|
}
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
|
|
// Prevent resetting password of inactive users
|
|
if (!user.is_active) {
|
|
return res.status(400).json({ error: 'Cannot reset password for inactive user' });
|
|
}
|
|
|
|
// Hash new password
|
|
const passwordHash = await bcrypt.hash(newPassword, 12);
|
|
|
|
// Update user password
|
|
await prisma.users.update({
|
|
where: { id: userId },
|
|
data: { passwordHash }
|
|
});
|
|
|
|
// Log the password reset action (you might want to add an audit log table)
|
|
console.log(`Password reset for user ${user.username} (${user.email}) by admin ${req.user.username}`);
|
|
|
|
res.json({
|
|
message: 'Password reset successfully',
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Password reset error:', error);
|
|
res.status(500).json({ error: 'Failed to reset password' });
|
|
}
|
|
});
|
|
|
|
// Public signup endpoint
|
|
router.post('/signup', [
|
|
body('username').isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
|
|
body('email').isEmail().withMessage('Valid email is required'),
|
|
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { username, email, password } = req.body;
|
|
|
|
// Check if user already exists
|
|
const existingUser = await prisma.users.findFirst({
|
|
where: {
|
|
OR: [
|
|
{ username },
|
|
{ email }
|
|
]
|
|
}
|
|
});
|
|
|
|
if (existingUser) {
|
|
return res.status(409).json({ error: 'Username or email already exists' });
|
|
}
|
|
|
|
// Hash password
|
|
const passwordHash = await bcrypt.hash(password, 12);
|
|
|
|
// Create user with default 'user' role
|
|
const user = await prisma.users.create({
|
|
data: {
|
|
id: uuidv4(),
|
|
username,
|
|
email,
|
|
password_hash: passwordHash,
|
|
role: 'user',
|
|
updated_at: new Date()
|
|
},
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
role: true,
|
|
is_active: true,
|
|
created_at: true
|
|
}
|
|
});
|
|
|
|
console.log(`New user registered: ${user.username} (${user.email})`);
|
|
|
|
// Generate token for immediate login
|
|
const token = generateToken(user.id);
|
|
|
|
res.status(201).json({
|
|
message: 'Account created successfully',
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Signup error:', error);
|
|
console.error('Signup error message:', error.message);
|
|
console.error('Signup error stack:', error.stack);
|
|
res.status(500).json({ error: 'Failed to create account' });
|
|
}
|
|
});
|
|
|
|
// Login
|
|
router.post('/login', [
|
|
body('username').notEmpty().withMessage('Username is required'),
|
|
body('password').notEmpty().withMessage('Password is required')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { username, password } = req.body;
|
|
|
|
// Find user by username or email
|
|
const user = await prisma.users.findFirst({
|
|
where: {
|
|
OR: [
|
|
{ username },
|
|
{ email: username }
|
|
],
|
|
is_active: true
|
|
},
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
password_hash: true,
|
|
role: true,
|
|
tfa_enabled: true
|
|
}
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(401).json({ error: 'Invalid credentials' });
|
|
}
|
|
|
|
// Verify password
|
|
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
|
if (!isValidPassword) {
|
|
return res.status(401).json({ error: 'Invalid credentials' });
|
|
}
|
|
|
|
// Check if TFA is enabled
|
|
if (user.tfa_enabled) {
|
|
return res.status(200).json({
|
|
message: 'TFA verification required',
|
|
requiresTfa: true,
|
|
username: user.username
|
|
});
|
|
}
|
|
|
|
// Update last login
|
|
await prisma.users.update({
|
|
where: { id: user.id },
|
|
data: {
|
|
last_login: new Date(),
|
|
updated_at: new Date()
|
|
}
|
|
});
|
|
|
|
// Generate token
|
|
const token = generateToken(user.id);
|
|
|
|
res.json({
|
|
message: 'Login successful',
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
res.status(500).json({ error: 'Login failed' });
|
|
}
|
|
});
|
|
|
|
// TFA verification for login
|
|
router.post('/verify-tfa', [
|
|
body('username').notEmpty().withMessage('Username is required'),
|
|
body('token').isLength({ min: 6, max: 6 }).withMessage('Token must be 6 digits'),
|
|
body('token').isNumeric().withMessage('Token must contain only numbers')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { username, token } = req.body;
|
|
|
|
// Find user
|
|
const user = await prisma.users.findFirst({
|
|
where: {
|
|
OR: [
|
|
{ username },
|
|
{ email: username }
|
|
],
|
|
is_active: true,
|
|
tfa_enabled: true
|
|
},
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
role: true,
|
|
tfa_secret: true,
|
|
tfa_backup_codes: true
|
|
}
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(401).json({ error: 'Invalid credentials or TFA not enabled' });
|
|
}
|
|
|
|
// Verify TFA token using the TFA routes logic
|
|
const speakeasy = require('speakeasy');
|
|
|
|
// Check if it's a backup code
|
|
const backupCodes = user.tfaBackupCodes ? JSON.parse(user.tfaBackupCodes) : [];
|
|
const isBackupCode = backupCodes.includes(token);
|
|
|
|
let verified = false;
|
|
|
|
if (isBackupCode) {
|
|
// Remove the used backup code
|
|
const updatedBackupCodes = backupCodes.filter(code => code !== token);
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: {
|
|
tfaBackupCodes: JSON.stringify(updatedBackupCodes)
|
|
}
|
|
});
|
|
verified = true;
|
|
} else {
|
|
// Verify TOTP token
|
|
verified = speakeasy.totp.verify({
|
|
secret: user.tfaSecret,
|
|
encoding: 'base32',
|
|
token: token,
|
|
window: 2
|
|
});
|
|
}
|
|
|
|
if (!verified) {
|
|
return res.status(401).json({ error: 'Invalid verification code' });
|
|
}
|
|
|
|
// Update last login
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { lastLogin: new Date() }
|
|
});
|
|
|
|
// Generate token
|
|
const jwtToken = generateToken(user.id);
|
|
|
|
res.json({
|
|
message: 'Login successful',
|
|
token: jwtToken,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('TFA verification error:', error);
|
|
res.status(500).json({ error: 'TFA verification failed' });
|
|
}
|
|
});
|
|
|
|
// Get current user profile
|
|
router.get('/profile', authenticateToken, async (req, res) => {
|
|
try {
|
|
res.json({
|
|
user: req.user
|
|
});
|
|
} catch (error) {
|
|
console.error('Get profile error:', error);
|
|
res.status(500).json({ error: 'Failed to get profile' });
|
|
}
|
|
});
|
|
|
|
// Update user profile
|
|
router.put('/profile', authenticateToken, [
|
|
body('username').optional().isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
|
|
body('email').optional().isEmail().withMessage('Valid email is required')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { username, email } = req.body;
|
|
const updateData = {};
|
|
|
|
if (username) updateData.username = username;
|
|
if (email) updateData.email = email;
|
|
|
|
// Check if username/email already exists (excluding current user)
|
|
if (username || email) {
|
|
const existingUser = await prisma.user.findFirst({
|
|
where: {
|
|
AND: [
|
|
{ id: { not: req.user.id } },
|
|
{
|
|
OR: [
|
|
...(username ? [{ username }] : []),
|
|
...(email ? [{ email }] : [])
|
|
]
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
if (existingUser) {
|
|
return res.status(409).json({ error: 'Username or email already exists' });
|
|
}
|
|
}
|
|
|
|
const updatedUser = await prisma.user.update({
|
|
where: { id: req.user.id },
|
|
data: updateData,
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
role: true,
|
|
isActive: true,
|
|
lastLogin: true,
|
|
updatedAt: true
|
|
}
|
|
});
|
|
|
|
res.json({
|
|
message: 'Profile updated successfully',
|
|
user: updatedUser
|
|
});
|
|
} catch (error) {
|
|
console.error('Update profile error:', error);
|
|
res.status(500).json({ error: 'Failed to update profile' });
|
|
}
|
|
});
|
|
|
|
// Change password
|
|
router.put('/change-password', authenticateToken, [
|
|
body('currentPassword').notEmpty().withMessage('Current password is required'),
|
|
body('newPassword').isLength({ min: 6 }).withMessage('New password must be at least 6 characters')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { currentPassword, newPassword } = req.body;
|
|
|
|
// Get user with password hash
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: req.user.id }
|
|
});
|
|
|
|
// Verify current password
|
|
const isValidPassword = await bcrypt.compare(currentPassword, user.passwordHash);
|
|
if (!isValidPassword) {
|
|
return res.status(401).json({ error: 'Current password is incorrect' });
|
|
}
|
|
|
|
// Hash new password
|
|
const newPasswordHash = await bcrypt.hash(newPassword, 12);
|
|
|
|
// Update password
|
|
await prisma.user.update({
|
|
where: { id: req.user.id },
|
|
data: { passwordHash: newPasswordHash }
|
|
});
|
|
|
|
res.json({
|
|
message: 'Password changed successfully'
|
|
});
|
|
} catch (error) {
|
|
console.error('Change password error:', error);
|
|
res.status(500).json({ error: 'Failed to change password' });
|
|
}
|
|
});
|
|
|
|
// Logout (client-side token removal)
|
|
router.post('/logout', authenticateToken, async (req, res) => {
|
|
try {
|
|
res.json({
|
|
message: 'Logout successful'
|
|
});
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
res.status(500).json({ error: 'Logout failed' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|