first commit

This commit is contained in:
Muhammad Ibrahim
2025-09-16 15:36:42 +01:00
commit c5332ce6b0
61 changed files with 21858 additions and 0 deletions

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