feat: implement role-based access control and auth routes
Some checks are pending
Docker Test / test (push) Waiting to run
Some checks are pending
Docker Test / test (push) Waiting to run
This commit implements the role-based access control middleware and authentication routes as per the project's requirements. It includes:
This commit is contained in:
parent
437bb1d504
commit
e278ee3da5
4 changed files with 274 additions and 0 deletions
35
backend/app.js
Normal file
35
backend/app.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const db = require('./db');
|
||||||
|
const authRoutes = require('./routes/auth');
|
||||||
|
const rolesRoutes = require('./routes/roles');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/auth', authRoutes);
|
||||||
|
app.use('/roles', rolesRoutes);
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({ error: 'Something went wrong!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use('*', (req, res) => {
|
||||||
|
res.status(404).json({ error: 'Route not found' });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
20
backend/middleware/requireRole.js
Normal file
20
backend/middleware/requireRole.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Middleware to require a specific role for an endpoint.
|
||||||
|
* @param {string[]} allowedRoles - Array of roles allowed to access the endpoint.
|
||||||
|
* @returns {function} Express middleware function.
|
||||||
|
*/
|
||||||
|
module.exports = (allowedRoles) => {
|
||||||
|
return (req, res, next) => {
|
||||||
|
const userRole = req.user?.role;
|
||||||
|
|
||||||
|
if (!userRole) {
|
||||||
|
return res.status(401).json({ error: 'Authorization required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowedRoles.includes(userRole)) {
|
||||||
|
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
140
backend/routes/auth.js
Normal file
140
backend/routes/auth.js
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { body, validationResult } = require('express-validator');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const db = require('../db');
|
||||||
|
const requireRole = require('../middleware/requireRole');
|
||||||
|
|
||||||
|
// Register a new user
|
||||||
|
router.post('/register', [
|
||||||
|
body('email').isEmail().normalizeEmail(),
|
||||||
|
body('password').isLength({ min: 8 }),
|
||||||
|
body('name').notEmpty()
|
||||||
|
], async (req, res) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email, password, name } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if user already exists
|
||||||
|
const existingUser = await db.query('SELECT id FROM users WHERE email = $1', [email]);
|
||||||
|
if (existingUser.rows.length > 0) {
|
||||||
|
return res.status(409).json({ error: 'User already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
|
// Insert new user with default role 'user'
|
||||||
|
const newUser = await db.query(
|
||||||
|
'INSERT INTO users (email, password_hash, name, role) VALUES ($1, $2, $3, $4) RETURNING id, email, name, role',
|
||||||
|
[email, hashedPassword, name, 'user']
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json({ user: newUser.rows[0] });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login user
|
||||||
|
router.post('/login', [
|
||||||
|
body('email').isEmail().normalizeEmail(),
|
||||||
|
body('password').notEmpty()
|
||||||
|
], async (req, res) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find user by email
|
||||||
|
const user = await db.query('SELECT id, email, password_hash, name, role FROM users WHERE email = $1', [email]);
|
||||||
|
|
||||||
|
if (user.rows.length === 0) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare passwords
|
||||||
|
const isValidPassword = await bcrypt.compare(password, user.rows[0].password_hash);
|
||||||
|
|
||||||
|
if (!isValidPassword) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token with role
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ userId: user.rows[0].id, email: user.rows[0].email, role: user.rows[0].role },
|
||||||
|
process.env.JWT_SECRET || 'default_secret',
|
||||||
|
{ expiresIn: '24h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user.rows[0].id,
|
||||||
|
email: user.rows[0].email,
|
||||||
|
name: user.rows[0].name,
|
||||||
|
role: user.rows[0].role
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get current user profile (requires authentication)
|
||||||
|
router.get('/profile', requireRole(['user', 'moderator', 'admin']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await db.query('SELECT id, email, name, role FROM users WHERE id = $1', [req.user.userId]);
|
||||||
|
|
||||||
|
if (user.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ user: user.rows[0] });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update current user profile (requires authentication)
|
||||||
|
router.put('/profile', requireRole(['user', 'moderator', 'admin']), [
|
||||||
|
body('name').optional().notEmpty()
|
||||||
|
], async (req, res) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { name } = req.body;
|
||||||
|
const userId = req.user.userId;
|
||||||
|
|
||||||
|
// Update user profile
|
||||||
|
const updatedUser = await db.query(
|
||||||
|
'UPDATE users SET name = $1 WHERE id = $2 RETURNING id, email, name, role',
|
||||||
|
[name, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updatedUser.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ user: updatedUser.rows[0] });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
79
backend/routes/roles.js
Normal file
79
backend/routes/roles.js
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require('../db');
|
||||||
|
const requireRole = require('../middleware/requireRole');
|
||||||
|
|
||||||
|
// Get all users (admin only)
|
||||||
|
router.get('/', requireRole(['admin']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const users = await db.query('SELECT id, email, name, role FROM users ORDER BY created_at DESC');
|
||||||
|
res.json({ users: users.rows });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Suspend a user (admin only)
|
||||||
|
router.put('/suspend/:userId', requireRole(['admin']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.params;
|
||||||
|
|
||||||
|
// Check if user exists
|
||||||
|
const existingUser = await db.query('SELECT id FROM users WHERE id = $1', [userId]);
|
||||||
|
if (existingUser.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suspend user
|
||||||
|
await db.query('UPDATE users SET suspended = true WHERE id = $1', [userId]);
|
||||||
|
|
||||||
|
// Log audit event
|
||||||
|
const auditEvent = {
|
||||||
|
actorUserId: req.user.userId,
|
||||||
|
action: 'USER_SUSPEND',
|
||||||
|
targetType: 'user',
|
||||||
|
targetId: userId,
|
||||||
|
reason: req.body.reason || 'No reason provided',
|
||||||
|
timestamp: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({ message: 'User suspended successfully' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unsuspend a user (admin only)
|
||||||
|
router.put('/unsuspend/:userId', requireRole(['admin']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.params;
|
||||||
|
|
||||||
|
// Check if user exists
|
||||||
|
const existingUser = await db.query('SELECT id FROM users WHERE id = $1', [userId]);
|
||||||
|
if (existingUser.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsuspend user
|
||||||
|
await db.query('UPDATE users SET suspended = false WHERE id = $1', [userId]);
|
||||||
|
|
||||||
|
// Log audit event
|
||||||
|
const auditEvent = {
|
||||||
|
actorUserId: req.user.userId,
|
||||||
|
action: 'USER_UNSUSPEND',
|
||||||
|
targetType: 'user',
|
||||||
|
targetId: userId,
|
||||||
|
reason: req.body.reason || 'No reason provided',
|
||||||
|
timestamp: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({ message: 'User unsuspended successfully' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue