const express = require('express'); const router = express.Router(); const { Role, User, Area, Project, Task, Tag, Note, InboxItem, TaskEvent, Action, Permission, View, ApiToken, Notification, RecurringCompletion, } = require('../models'); const { isAdmin } = require('../services/rolesService'); const { logError } = require('../services/logService'); // POST /api/admin/set-admin-role // Body: { user_id: number, is_admin: boolean } router.post('/admin/set-admin-role', async (req, res) => { try { const requesterId = req.currentUser?.id || req.session?.userId; if (!requesterId) return res.status(401).json({ error: 'Authentication required' }); // Fetch user to get uid for isAdmin check const requester = await User.findByPk(requesterId, { attributes: ['uid'], }); if (!requester) return res.status(401).json({ error: 'Authentication required' }); // Allow if requester is already admin OR if there are no roles yet (bootstrap) const requesterIsAdmin = await isAdmin(requester.uid); const existingRolesCount = await Role.count(); if (!requesterIsAdmin && existingRolesCount > 0) { return res.status(403).json({ error: 'Forbidden' }); } const { user_id, is_admin } = req.body; if (!user_id || typeof is_admin !== 'boolean') { return res .status(400) .json({ error: 'user_id and is_admin are required' }); } const user = await User.findByPk(user_id); if (!user) return res.status(400).json({ error: 'Invalid user_id' }); const [role] = await Role.findOrCreate({ where: { user_id }, defaults: { user_id, is_admin }, }); if (role.is_admin !== is_admin) { role.is_admin = is_admin; await role.save(); } res.json({ user_id, is_admin: role.is_admin }); } catch (err) { logError('Error setting admin role:', err); res.status(500).json({ error: 'Internal server error' }); } }); module.exports = router; // --- Admin Users Management --- // NOTE: app.js already mounts this router under requireAuth // Middleware to ensure admin access async function requireAdmin(req, res, next) { try { const requesterId = req.currentUser?.id || req.session?.userId; if (!requesterId) return res.status(401).json({ error: 'Authentication required' }); // Fetch user to get uid for isAdmin check const user = await User.findByPk(requesterId, { attributes: ['uid'] }); if (!user) return res.status(401).json({ error: 'Authentication required' }); const admin = await isAdmin(user.uid); if (!admin) return res.status(403).json({ error: 'Forbidden' }); next(); } catch (err) { next(err); } } // GET /api/admin/users - list users with role and creation date router.get('/admin/users', requireAdmin, async (req, res) => { try { const users = await User.findAll({ attributes: ['id', 'email', 'name', 'surname', 'created_at'], }); // Fetch roles in bulk const roles = await Role.findAll({ attributes: ['user_id', 'is_admin'], }); const userIdToRole = new Map(roles.map((r) => [r.user_id, r.is_admin])); const result = users.map((u) => ({ id: u.id, email: u.email, name: u.name, surname: u.surname, created_at: u.created_at, role: userIdToRole.get(u.id) ? 'admin' : 'user', })); res.json(result); } catch (err) { logError('Error listing users:', err); res.status(500).json({ error: 'Internal server error' }); } }); // POST /api/admin/users - create a user (default role: user) router.post('/admin/users', requireAdmin, async (req, res) => { try { const { email, password, name, surname, role } = req.body || {}; if (!email || !password) { return res .status(400) .json({ error: 'Email and password are required' }); } // Very basic validation consistent with login rules if (typeof email !== 'string' || !email.includes('@')) { return res.status(400).json({ error: 'Invalid email' }); } if (typeof password !== 'string' || password.length < 6) { return res .status(400) .json({ error: 'Password must be at least 6 characters' }); } // Create user; model hook will hash password const userData = { email, password }; if (name) userData.name = name; if (surname) userData.surname = surname; const user = await User.create(userData); // Optionally assign admin role if requested and allowed const makeAdmin = role === 'admin'; if (makeAdmin) { // Find or create role, and ensure is_admin is true const [userRole, roleCreated] = await Role.findOrCreate({ where: { user_id: user.id }, defaults: { user_id: user.id, is_admin: true }, }); // Update to admin if role exists but is not admin if (!roleCreated && !userRole.is_admin) { userRole.is_admin = true; await userRole.save(); } } res.status(201).json({ id: user.id, email: user.email, name: user.name, surname: user.surname, created_at: user.created_at, role: makeAdmin ? 'admin' : 'user', }); } catch (err) { logError('Error creating user:', err); // Unique constraint if (err?.name === 'SequelizeUniqueConstraintError') { return res.status(409).json({ error: 'Email already exists' }); } res.status(400).json({ error: 'There was a problem creating the user.', }); } }); // PUT /api/admin/users/:id - update a user router.put('/admin/users/:id', requireAdmin, async (req, res) => { try { const id = parseInt(req.params.id, 10); if (!Number.isFinite(id)) return res.status(400).json({ error: 'Invalid user id' }); const user = await User.findByPk(id); if (!user) return res.status(404).json({ error: 'User not found' }); const { email, password, name, surname, role } = req.body || {}; // Update email if provided if (email !== undefined && email !== null) { if (typeof email !== 'string' || !email.includes('@')) { return res.status(400).json({ error: 'Invalid email' }); } user.email = email; } // Update password if provided if (password && password.trim() !== '') { if (typeof password !== 'string' || password.length < 6) { return res .status(400) .json({ error: 'Password must be at least 6 characters' }); } user.password = password; } // Update name and surname - handle empty strings properly if (name !== undefined) user.name = name || null; if (surname !== undefined) user.surname = surname || null; await user.save(); // Update role if provided if (role !== undefined) { const makeAdmin = role === 'admin'; const [userRole] = await Role.findOrCreate({ where: { user_id: user.id }, defaults: { user_id: user.id, is_admin: makeAdmin }, }); if (userRole.is_admin !== makeAdmin) { userRole.is_admin = makeAdmin; await userRole.save(); } } // Fetch updated role const userRole = await Role.findOne({ where: { user_id: user.id } }); res.json({ id: user.id, email: user.email, name: user.name, surname: user.surname, created_at: user.created_at, role: userRole?.is_admin ? 'admin' : 'user', }); } catch (err) { logError('Error updating user:', err); // Unique constraint if (err?.name === 'SequelizeUniqueConstraintError') { return res.status(409).json({ error: 'Email already exists' }); } res.status(400).json({ error: 'There was a problem updating the user.', }); } }); // DELETE /api/admin/users/:id - delete a user, prevent self-delete router.delete('/admin/users/:id', requireAdmin, async (req, res) => { const { sequelize } = require('../models'); const transaction = await sequelize.transaction(); try { const id = parseInt(req.params.id, 10); const requesterId = req.currentUser?.id || req.session?.userId; if (!Number.isFinite(id)) { await transaction.rollback(); return res.status(400).json({ error: 'Invalid user id' }); } if (id === requesterId) { await transaction.rollback(); return res .status(400) .json({ error: 'Cannot delete your own account' }); } const user = await User.findByPk(id, { transaction }); if (!user) { await transaction.rollback(); return res.status(404).json({ error: 'User not found' }); } // Prevent deleting the last remaining admin const targetRole = await Role.findOne({ where: { user_id: id }, transaction, }); if (targetRole?.is_admin) { const adminCount = await Role.count({ where: { is_admin: true }, transaction, }); if (adminCount <= 1) { await transaction.rollback(); return res .status(400) .json({ error: 'Cannot delete the last remaining admin' }); } } await TaskEvent.destroy({ where: { user_id: id }, transaction }); const userTasks = await Task.findAll({ where: { user_id: id }, attributes: ['id'], transaction, }); const taskIds = userTasks.map((t) => t.id); if (taskIds.length > 0) { await RecurringCompletion.destroy({ where: { task_id: taskIds }, transaction, }); } await Task.destroy({ where: { user_id: id }, transaction }); await Note.destroy({ where: { user_id: id }, transaction }); await Project.destroy({ where: { user_id: id }, transaction }); await Area.destroy({ where: { user_id: id }, transaction }); await Tag.destroy({ where: { user_id: id }, transaction }); await InboxItem.destroy({ where: { user_id: id }, transaction }); await View.destroy({ where: { user_id: id }, transaction }); await Notification.destroy({ where: { user_id: id }, transaction }); await ApiToken.destroy({ where: { user_id: id }, transaction }); await Permission.destroy({ where: { user_id: id }, transaction }); await Permission.destroy({ where: { granted_by_user_id: id }, transaction, }); await Action.destroy({ where: { actor_user_id: id }, transaction }); await Action.destroy({ where: { target_user_id: id }, transaction }); await Role.destroy({ where: { user_id: id }, transaction }); await user.destroy({ transaction }); await transaction.commit(); res.status(204).send(); } catch (err) { await transaction.rollback(); logError('Error deleting user:', err); res.status(400).json({ error: 'There was a problem deleting the user.', }); } }); // POST /api/admin/toggle-registration - toggle registration setting router.post('/admin/toggle-registration', requireAdmin, async (req, res) => { try { const { enabled } = req.body; if (typeof enabled !== 'boolean') { return res .status(400) .json({ error: 'enabled must be a boolean value' }); } const { setRegistrationEnabled, } = require('../services/registrationService'); await setRegistrationEnabled(enabled); res.json({ enabled }); } catch (err) { logError('Error toggling registration:', err); res.status(500).json({ error: 'Internal server error' }); } });