mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-12 18:06:39 +00:00
first commit
This commit is contained in:
452
backend/src/routes/authRoutes.js
Normal file
452
backend/src/routes/authRoutes.js
Normal file
@@ -0,0 +1,452 @@
|
||||
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 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.user.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.rolePermissions.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.user.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.user.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.rolePermissions.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.user.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.user.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.user.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.user.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.user.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.user.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.user.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' });
|
||||
}
|
||||
});
|
||||
|
||||
// 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.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ username },
|
||||
{ email: username }
|
||||
],
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValidPassword = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLogin: 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' });
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
Reference in New Issue
Block a user