chore: estructura inicial del MVP
Backend (Express + MariaDB) y frontend (React + Vite, identidad Fluent/Windows) con el nucleo funcional: seguridad, organizacion, proyectos, actividades, subtareas, dependencias, dashboard, calendario, gantt, comentarios, adjuntos, notificaciones in-app y catalogos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { ok, asyncHandler } from '../../utils/http.js';
|
||||
import * as service from './service.js';
|
||||
|
||||
export const getUsers = asyncHandler(async (req, res) => ok(res, await service.listUsers()));
|
||||
export const getRoles = asyncHandler(async (req, res) => ok(res, await service.listRoles()));
|
||||
|
||||
export const postUser = asyncHandler(async (req, res) => {
|
||||
const user = await service.createUser(req.body, req.currentUser.id);
|
||||
ok(res, user, 201);
|
||||
});
|
||||
|
||||
export const putUser = asyncHandler(async (req, res) => {
|
||||
const user = await service.updateUser(Number(req.params.id), req.body, req.currentUser.id);
|
||||
ok(res, user);
|
||||
});
|
||||
|
||||
export const patchUserActive = asyncHandler(async (req, res) => {
|
||||
const user = await service.setUserActive(Number(req.params.id), !!req.body.isActive, req.currentUser.id);
|
||||
ok(res, user);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { pool } from '../../config/db.js';
|
||||
|
||||
export async function findAll() {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT u.id, u.name, u.email, u.role_id, r.code AS role_code, r.name AS role_name,
|
||||
u.department_id, d.name AS department_name, u.position, u.is_active, u.created_at
|
||||
FROM users u
|
||||
JOIN roles r ON r.id = u.role_id
|
||||
LEFT JOIN departments d ON d.id = u.department_id
|
||||
ORDER BY u.name`
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function findById(id) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT u.id, u.name, u.email, u.role_id, r.code AS role_code, u.department_id, u.position, u.is_active
|
||||
FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = ?`,
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function findByEmail(email) {
|
||||
const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function create({ name, email, passwordHash, roleId, departmentId, position }) {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO users (name, email, password_hash, role_id, department_id, position, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 1)`,
|
||||
[name, email, passwordHash, roleId, departmentId || null, position || null]
|
||||
);
|
||||
return findById(result.insertId);
|
||||
}
|
||||
|
||||
export async function update(id, { name, roleId, departmentId, position }) {
|
||||
await pool.query(
|
||||
`UPDATE users SET name = ?, role_id = ?, department_id = ?, position = ? WHERE id = ?`,
|
||||
[name, roleId, departmentId || null, position || null, id]
|
||||
);
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
export async function setActive(id, isActive) {
|
||||
await pool.query('UPDATE users SET is_active = ? WHERE id = ?', [isActive ? 1 : 0, id]);
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
export async function listRoles() {
|
||||
const [rows] = await pool.query('SELECT id, code, name FROM roles ORDER BY id');
|
||||
return rows;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth, requireRole, CAN_MANAGE_USERS } from '../../middleware/auth.js';
|
||||
import { getUsers, getRoles, postUser, putUser, patchUserActive } from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
router.get('/', getUsers);
|
||||
router.get('/roles', getRoles);
|
||||
router.post('/', requireRole(...CAN_MANAGE_USERS), postUser);
|
||||
router.put('/:id', requireRole(...CAN_MANAGE_USERS), putUser);
|
||||
router.patch('/:id/active', requireRole(...CAN_MANAGE_USERS), patchUserActive);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,41 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import * as repo from './repository.js';
|
||||
import { HttpError } from '../../utils/http.js';
|
||||
import { logAudit } from '../../utils/audit.js';
|
||||
|
||||
export const listUsers = () => repo.findAll();
|
||||
export const listRoles = () => repo.listRoles();
|
||||
|
||||
export async function createUser(payload, actorId) {
|
||||
if (!payload.name || !payload.email || !payload.password || !payload.roleId) {
|
||||
throw new HttpError(400, 'Nombre, email, clave y rol son obligatorios');
|
||||
}
|
||||
const existing = await repo.findByEmail(payload.email);
|
||||
if (existing) throw new HttpError(400, 'Ya existe un usuario con ese email');
|
||||
|
||||
const passwordHash = await bcrypt.hash(payload.password, 12);
|
||||
const user = await repo.create({ ...payload, passwordHash });
|
||||
await logAudit({ userId: actorId, action: 'create', entityType: 'user', entityId: user.id });
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function updateUser(id, payload, actorId) {
|
||||
const existing = await repo.findById(id);
|
||||
if (!existing) throw new HttpError(404, 'Usuario no encontrado');
|
||||
const user = await repo.update(id, payload);
|
||||
await logAudit({ userId: actorId, action: 'update', entityType: 'user', entityId: id });
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function setUserActive(id, isActive, actorId) {
|
||||
const existing = await repo.findById(id);
|
||||
if (!existing) throw new HttpError(404, 'Usuario no encontrado');
|
||||
const user = await repo.setActive(id, isActive);
|
||||
await logAudit({
|
||||
userId: actorId,
|
||||
action: isActive ? 'activate' : 'deactivate',
|
||||
entityType: 'user',
|
||||
entityId: id,
|
||||
});
|
||||
return user;
|
||||
}
|
||||
Reference in New Issue
Block a user