implement authentication and authorization

This commit is contained in:
Yuri
2026-01-01 15:36:38 -05:00
parent 46c68813ec
commit e1a197f2be
16 changed files with 1704 additions and 2 deletions
+261
View File
@@ -0,0 +1,261 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const auth = require('../auth');
// Configure Passport strategies
auth.configureLocalStrategy(
async (username) => await db.users.getByUsername(username),
async (password, hash) => await auth.verifyPassword(password, hash)
);
auth.configureJwtStrategy(
async (id) => await db.users.getById(id)
);
/**
* Check if initial setup is required
* GET /api/auth/setup-required
*/
router.get('/setup-required', async (req, res) => {
try {
const userCount = await db.users.count();
res.json({ setupRequired: userCount === 0 });
} catch (err) {
console.error('Error in /setup-required:', err);
res.status(500).json({ error: 'Server error' });
}
});
/**
* Initial setup - Create admin user
* POST /api/auth/setup
*/
router.post('/setup', async (req, res) => {
try {
const userCount = await db.users.count();
// Check if setup already done
if (userCount > 0) {
return res.status(400).json({ error: 'Setup already completed' });
}
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters' });
}
// Create admin user
const passwordHash = await auth.hashPassword(password);
const adminUser = await db.users.create({
username,
passwordHash,
role: 'admin'
});
// Generate token for immediate login
const token = auth.generateToken(adminUser);
res.status(201).json({
message: 'Admin user created successfully',
token,
user: adminUser
});
} catch (err) {
console.error('Error in /setup:', err);
res.status(500).json({ error: err.message || 'Server error' });
}
});
/**
* Login with Passport Local Strategy
* POST /api/auth/login
*/
router.post('/login', (req, res, next) => {
auth.passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
console.error('Login error:', err);
return res.status(500).json({ error: 'Server error' });
}
if (!user) {
return res.status(401).json({ error: info?.message || 'Invalid credentials' });
}
// Generate JWT token
const token = auth.generateToken(user);
res.json({
token,
user: {
id: user.id,
username: user.username,
role: user.role
}
});
})(req, res, next);
});
/**
* Logout (client-side handles token removal)
* POST /api/auth/logout
*/
router.post('/logout', (req, res) => {
// With JWT, logout is handled client-side by removing the token
// This endpoint exists for consistency and future server-side token blacklisting
res.json({ success: true, message: 'Logged out successfully' });
});
/**
* Get current user
* GET /api/auth/me
*/
router.get('/me', auth.requireAuth, async (req, res) => {
try {
const user = await db.users.getById(req.user.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
id: user.id,
username: user.username,
role: user.role
});
} catch (err) {
console.error('Error in /me:', err);
res.status(500).json({ error: 'Server error' });
}
});
/**
* Get all users (admin only)
* GET /api/auth/users
*/
router.get('/users', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const allUsers = await db.users.getAll();
// Remove password hashes
const users = allUsers.map(u => {
const { passwordHash, ...userWithoutPassword } = u;
return userWithoutPassword;
});
res.json(users);
} catch (err) {
console.error('Error fetching users:', err);
res.status(500).json({ error: 'Server error' });
}
});
/**
* Create a new user (admin only)
* POST /api/auth/users
*/
router.post('/users', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { username, password, role } = req.body;
if (!username || !password || !role) {
return res.status(400).json({ error: 'Username, password, and role are required' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters' });
}
if (!['admin', 'viewer'].includes(role)) {
return res.status(400).json({ error: 'Role must be either "admin" or "viewer"' });
}
const passwordHash = await auth.hashPassword(password);
const newUser = await db.users.create({
username,
passwordHash,
role
});
res.status(201).json(newUser);
} catch (err) {
console.error('Error creating user:', err);
res.status(500).json({ error: err.message || 'Server error' });
}
});
/**
* Update a user (admin only)
* PUT /api/auth/users/:id
*/
router.put('/users/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { id } = req.params;
const { username, password, role } = req.body;
const updates = {};
if (username) {
updates.username = username;
}
if (password) {
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters' });
}
updates.passwordHash = await auth.hashPassword(password);
}
if (role) {
if (!['admin', 'viewer'].includes(role)) {
return res.status(400).json({ error: 'Role must be either "admin" or "viewer"' });
}
// Prevent removing admin role from the last admin
const user = await db.users.getById(id);
if (user && user.role === 'admin' && role !== 'admin') {
const allUsers = await db.users.getAll();
const adminCount = allUsers.filter(u => u.role === 'admin').length;
if (adminCount <= 1) {
return res.status(400).json({ error: 'Cannot remove admin role from the last admin user' });
}
}
updates.role = role;
}
const updatedUser = await db.users.update(id, updates);
res.json(updatedUser);
} catch (err) {
console.error('Error updating user:', err);
res.status(500).json({ error: err.message || 'Server error' });
}
});
/**
* Delete a user (admin only)
* DELETE /api/auth/users/:id
*/
router.delete('/users/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { id } = req.params;
// Prevent deleting yourself
if (parseInt(id) === req.user.id) {
return res.status(400).json({ error: 'Cannot delete your own account' });
}
await db.users.delete(id);
res.json({ success: true, message: 'User deleted successfully' });
} catch (err) {
console.error('Error deleting user:', err);
res.status(500).json({ error: err.message || 'Server error' });
}
});
module.exports = router;
+186
View File
@@ -0,0 +1,186 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const auth = require('../auth');
/**
* Get all users (admin only)
* GET /api/users
*/
router.get('/', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const data = await db.loadDb();
const users = (data.users || []).map(u => ({
id: u.id,
username: u.username,
role: u.role,
createdAt: u.createdAt
}));
res.json(users);
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
/**
* Create user (admin only)
* POST /api/users
*/
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { username, password, role } = req.body;
if (!username || !password || !role) {
return res.status(400).json({ error: 'Username, password, and role required' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters' });
}
if (!['admin', 'viewer'].includes(role)) {
return res.status(400).json({ error: 'Role must be admin or viewer' });
}
const data = await db.loadDb();
// Check if username exists
if (data.users?.some(u => u.username === username)) {
return res.status(400).json({ error: 'Username already exists' });
}
// Create user
const passwordHash = await auth.hashPassword(password);
const newUser = {
id: data.nextUserId || (data.users?.length || 0) + 1,
username,
passwordHash,
role,
createdAt: new Date().toISOString()
};
data.users = data.users || [];
data.users.push(newUser);
data.nextUserId = newUser.id + 1;
await db.saveDb(data);
res.json({
id: newUser.id,
username: newUser.username,
role: newUser.role,
createdAt: newUser.createdAt
});
} catch (err) {
console.error('Create user error:', err);
res.status(500).json({ error: 'Server error' });
}
});
/**
* Update user (admin only)
* PUT /api/users/:id
*/
router.put('/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const userId = parseInt(req.params.id);
const { username, password, role } = req.body;
const data = await db.loadDb();
const userIndex = data.users?.findIndex(u => u.id === userId);
if (userIndex === -1 || userIndex === undefined) {
return res.status(404).json({ error: 'User not found' });
}
const user = data.users[userIndex];
// Update username if provided
if (username && username !== user.username) {
// Check if new username exists
if (data.users.some(u => u.username === username && u.id !== userId)) {
return res.status(400).json({ error: 'Username already exists' });
}
user.username = username;
}
// Update password if provided
if (password) {
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters' });
}
user.passwordHash = await auth.hashPassword(password);
}
// Update role if provided
if (role) {
if (!['admin', 'viewer'].includes(role)) {
return res.status(400).json({ error: 'Role must be admin or viewer' });
}
// Prevent removing last admin
if (user.role === 'admin' && role !== 'admin') {
const adminCount = data.users.filter(u => u.role === 'admin').length;
if (adminCount <= 1) {
return res.status(400).json({ error: 'Cannot remove last admin user' });
}
}
user.role = role;
}
await db.saveDb(data);
res.json({
id: user.id,
username: user.username,
role: user.role,
createdAt: user.createdAt
});
} catch (err) {
console.error('Update user error:', err);
res.status(500).json({ error: 'Server error' });
}
});
/**
* Delete user (admin only)
* DELETE /api/users/:id
*/
router.delete('/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const userId = parseInt(req.params.id);
const data = await db.loadDb();
const userIndex = data.users?.findIndex(u => u.id === userId);
if (userIndex === -1 || userIndex === undefined) {
return res.status(404).json({ error: 'User not found' });
}
const user = data.users[userIndex];
// Prevent deleting yourself
if (user.id === req.session.userId) {
return res.status(400).json({ error: 'Cannot delete your own account' });
}
// Prevent deleting last admin
if (user.role === 'admin') {
const adminCount = data.users.filter(u => u.role === 'admin').length;
if (adminCount <= 1) {
return res.status(400).json({ error: 'Cannot delete last admin user' });
}
}
data.users.splice(userIndex, 1);
await db.saveDb(data);
res.json({ success: true });
} catch (err) {
console.error('Delete user error:', err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;