feat: AR-ProjectManagement initial commit — JavaScript (Node.js / React) Express + Prisma (backend) / React + Axios (frontend)
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias de sistema para Prisma
|
||||
RUN apk add --no-cache openssl
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npx prisma generate
|
||||
|
||||
EXPOSE ${PORT:-4000}
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "ar-pm-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "AR Project Management - Backend API",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon src/index.js",
|
||||
"start": "node src/index.js",
|
||||
"prisma:migrate": "npx prisma migrate dev",
|
||||
"prisma:studio": "npx prisma studio",
|
||||
"seed": "node prisma/seed.js",
|
||||
"test": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.14.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "^7.3.1",
|
||||
"express-validator": "^7.1.0",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"speakeasy": "^2.0.0",
|
||||
"uuid": "^10.0.0",
|
||||
"winston": "^3.13.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.3",
|
||||
"prisma": "^5.14.0",
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^7.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// AR-ProjectManagement — Prisma Schema
|
||||
// Base de datos: PostgreSQL 16
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ENUMS
|
||||
// ============================================================
|
||||
enum Role {
|
||||
SUPER_ADMIN
|
||||
DIRECTOR
|
||||
GERENTE
|
||||
JEFE_AREA
|
||||
COLABORADOR
|
||||
FINANCIERO
|
||||
AUDITOR
|
||||
}
|
||||
|
||||
enum ProjectStatus {
|
||||
ACTIVO
|
||||
PAUSADO
|
||||
COMPLETADO
|
||||
EN_RIESGO
|
||||
CANCELADO
|
||||
}
|
||||
|
||||
enum TaskStatus {
|
||||
PENDIENTE
|
||||
EN_PROCESO
|
||||
BLOQUEADA
|
||||
EN_VERIFICACION
|
||||
NO_CONFORME
|
||||
APROBADO_TECNICO
|
||||
ACEPTADO_FORMAL
|
||||
COMPLETADA
|
||||
}
|
||||
|
||||
enum Priority {
|
||||
CRITICO
|
||||
ALTO
|
||||
MEDIO
|
||||
BAJO
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MODULO 1: USUARIOS Y AUTENTICACION
|
||||
// ============================================================
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
passwordHash String @map("password_hash")
|
||||
firstName String @map("first_name")
|
||||
lastName String @map("last_name")
|
||||
role Role @default(COLABORADOR)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
isTwoFactorEnabled Boolean @default(false) @map("is_two_factor_enabled")
|
||||
twoFactorSecret String? @map("two_factor_secret")
|
||||
loginAttempts Int @default(0) @map("login_attempts")
|
||||
lockedUntil DateTime? @map("locked_until")
|
||||
lastLoginAt DateTime? @map("last_login_at")
|
||||
lastLoginIp String? @map("last_login_ip")
|
||||
avatarUrl String? @map("avatar_url")
|
||||
phone String?
|
||||
department String?
|
||||
jobTitle String? @map("job_title")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
refreshTokens RefreshToken[]
|
||||
auditLogs AuditLog[]
|
||||
projectMembers ProjectMember[]
|
||||
tasksAssigned Task[] @relation("TaskAssignee")
|
||||
tasksCreated Task[] @relation("TaskCreator")
|
||||
comments Comment[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(uuid())
|
||||
token String @unique
|
||||
userId String @map("user_id")
|
||||
expiresAt DateTime @map("expires_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
revokedAt DateTime? @map("revoked_at")
|
||||
ipAddress String? @map("ip_address")
|
||||
userAgent String? @map("user_agent")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("refresh_tokens")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LOG DE AUDITORIA INMUTABLE
|
||||
// ============================================================
|
||||
model AuditLog {
|
||||
id String @id @default(uuid())
|
||||
userId String? @map("user_id")
|
||||
action String
|
||||
entity String
|
||||
entityId String? @map("entity_id")
|
||||
oldValues Json? @map("old_values")
|
||||
newValues Json? @map("new_values")
|
||||
ipAddress String? @map("ip_address")
|
||||
userAgent String? @map("user_agent")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("audit_logs")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MODULO 3: PROYECTOS
|
||||
// ============================================================
|
||||
model Project {
|
||||
id String @id @default(uuid())
|
||||
code String @unique
|
||||
name String
|
||||
description String?
|
||||
type String
|
||||
category String
|
||||
client String?
|
||||
startDate DateTime @map("start_date")
|
||||
plannedEndDate DateTime @map("planned_end_date")
|
||||
actualEndDate DateTime? @map("actual_end_date")
|
||||
budget Decimal @db.Decimal(15, 2)
|
||||
contingencyPct Decimal @default(10) @map("contingency_pct") @db.Decimal(5, 2)
|
||||
currency String @default("USD")
|
||||
priority Priority @default(MEDIO)
|
||||
status ProjectStatus @default(ACTIVO)
|
||||
currentPhase String? @map("current_phase")
|
||||
methodology String?
|
||||
tags String[]
|
||||
ragStatus String @default("VERDE") @map("rag_status")
|
||||
progressPct Decimal @default(0) @map("progress_pct") @db.Decimal(5, 2)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
members ProjectMember[]
|
||||
tasks Task[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
|
||||
model ProjectMember {
|
||||
id String @id @default(uuid())
|
||||
projectId String @map("project_id")
|
||||
userId String @map("user_id")
|
||||
role String
|
||||
joinedAt DateTime @default(now()) @map("joined_at")
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([projectId, userId])
|
||||
@@map("project_members")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MODULO 4: TAREAS
|
||||
// ============================================================
|
||||
model Task {
|
||||
id String @id @default(uuid())
|
||||
projectId String @map("project_id")
|
||||
parentId String? @map("parent_id")
|
||||
wbsId String? @map("wbs_id")
|
||||
title String
|
||||
description String?
|
||||
assigneeId String? @map("assignee_id")
|
||||
createdById String @map("created_by_id")
|
||||
startDate DateTime? @map("start_date")
|
||||
dueDate DateTime? @map("due_date")
|
||||
estimatedHours Decimal? @map("estimated_hours") @db.Decimal(8, 2)
|
||||
actualHours Decimal? @map("actual_hours") @db.Decimal(8, 2)
|
||||
progressPct Decimal @default(0) @map("progress_pct") @db.Decimal(5, 2)
|
||||
status TaskStatus @default(PENDIENTE)
|
||||
priority Priority @default(MEDIO)
|
||||
estimatedCost Decimal? @map("estimated_cost") @db.Decimal(15, 2)
|
||||
actualCost Decimal? @map("actual_cost") @db.Decimal(15, 2)
|
||||
tags String[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id])
|
||||
creator User @relation("TaskCreator", fields: [createdById], references: [id])
|
||||
parent Task? @relation("TaskChildren", fields: [parentId], references: [id])
|
||||
children Task[] @relation("TaskChildren")
|
||||
comments Comment[]
|
||||
|
||||
@@map("tasks")
|
||||
}
|
||||
|
||||
model Comment {
|
||||
id String @id @default(uuid())
|
||||
taskId String @map("task_id")
|
||||
userId String @map("user_id")
|
||||
content String
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("comments")
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// Users
|
||||
const passwordHash = await bcrypt.hash('Admin123!', 12);
|
||||
|
||||
const superAdmin = await prisma.user.upsert({
|
||||
where: { email: 'admin@arpm.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'admin@arpm.com',
|
||||
passwordHash,
|
||||
firstName: 'Admin',
|
||||
lastName: 'Sistema',
|
||||
role: 'SUPER_ADMIN',
|
||||
},
|
||||
});
|
||||
|
||||
const director = await prisma.user.upsert({
|
||||
where: { email: 'director@arpm.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'director@arpm.com',
|
||||
passwordHash,
|
||||
firstName: 'Carlos',
|
||||
lastName: 'Ramírez',
|
||||
role: 'DIRECTOR',
|
||||
department: 'Dirección',
|
||||
jobTitle: 'Director General',
|
||||
},
|
||||
});
|
||||
|
||||
const gerente = await prisma.user.upsert({
|
||||
where: { email: 'gerente@arpm.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'gerente@arpm.com',
|
||||
passwordHash,
|
||||
firstName: 'Ana',
|
||||
lastName: 'Torres',
|
||||
role: 'GERENTE',
|
||||
department: 'Proyectos',
|
||||
jobTitle: 'Gerente de Proyectos',
|
||||
},
|
||||
});
|
||||
|
||||
const colaborador = await prisma.user.upsert({
|
||||
where: { email: 'colaborador@arpm.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'colaborador@arpm.com',
|
||||
passwordHash,
|
||||
firstName: 'Luis',
|
||||
lastName: 'Mendoza',
|
||||
role: 'COLABORADOR',
|
||||
department: 'Ingeniería',
|
||||
jobTitle: 'Desarrollador Senior',
|
||||
},
|
||||
});
|
||||
|
||||
// Projects
|
||||
const proj1 = await prisma.project.upsert({
|
||||
where: { code: 'PRY-2026-001' },
|
||||
update: {},
|
||||
create: {
|
||||
code: 'PRY-2026-001',
|
||||
name: 'Construcción Torre Corporativa Norte',
|
||||
description: 'Proyecto de construcción de edificio de 20 pisos para uso corporativo',
|
||||
type: 'Construcción',
|
||||
category: 'Infraestructura',
|
||||
client: 'Inmobiliaria Del Valle',
|
||||
startDate: new Date('2026-01-15'),
|
||||
plannedEndDate: new Date('2027-12-31'),
|
||||
budget: 15000000.00,
|
||||
priority: 'ALTO',
|
||||
status: 'ACTIVO',
|
||||
currentPhase: 'Cimentación',
|
||||
methodology: 'PMI',
|
||||
ragStatus: 'VERDE',
|
||||
progressPct: 12.5,
|
||||
tags: ['construcción', 'corporativo', 'norte'],
|
||||
},
|
||||
});
|
||||
|
||||
const proj2 = await prisma.project.upsert({
|
||||
where: { code: 'PRY-2026-002' },
|
||||
update: {},
|
||||
create: {
|
||||
code: 'PRY-2026-002',
|
||||
name: 'Álbum Musical "Horizontes"',
|
||||
description: 'Producción completa del cuarto álbum del artista Marco Solís',
|
||||
type: 'Producción Musical',
|
||||
category: 'Entretenimiento',
|
||||
client: 'Sony Music Latin',
|
||||
startDate: new Date('2026-03-01'),
|
||||
plannedEndDate: new Date('2026-09-30'),
|
||||
budget: 250000.00,
|
||||
priority: 'MEDIO',
|
||||
status: 'ACTIVO',
|
||||
currentPhase: 'Grabación',
|
||||
methodology: 'Agile',
|
||||
ragStatus: 'AMARILLO',
|
||||
progressPct: 35.0,
|
||||
tags: ['música', 'álbum', 'producción'],
|
||||
},
|
||||
});
|
||||
|
||||
const proj3 = await prisma.project.upsert({
|
||||
where: { code: 'PRY-2026-003' },
|
||||
update: {},
|
||||
create: {
|
||||
code: 'PRY-2026-003',
|
||||
name: 'Implementación ERP Empresarial',
|
||||
description: 'Migración y modernización del sistema ERP corporativo',
|
||||
type: 'Software',
|
||||
category: 'Tecnología',
|
||||
client: 'Grupo Empresarial XYZ',
|
||||
startDate: new Date('2026-02-01'),
|
||||
plannedEndDate: new Date('2026-11-30'),
|
||||
budget: 800000.00,
|
||||
priority: 'CRITICO',
|
||||
status: 'EN_RIESGO',
|
||||
currentPhase: 'Desarrollo',
|
||||
methodology: 'Scrum',
|
||||
ragStatus: 'ROJO',
|
||||
progressPct: 48.0,
|
||||
tags: ['erp', 'software', 'migración'],
|
||||
},
|
||||
});
|
||||
|
||||
// Project members
|
||||
for (const proj of [proj1, proj2, proj3]) {
|
||||
await prisma.projectMember.upsert({
|
||||
where: { projectId_userId: { projectId: proj.id, userId: gerente.id } },
|
||||
update: {},
|
||||
create: { projectId: proj.id, userId: gerente.id, role: 'GERENTE' },
|
||||
});
|
||||
await prisma.projectMember.upsert({
|
||||
where: { projectId_userId: { projectId: proj.id, userId: colaborador.id } },
|
||||
update: {},
|
||||
create: { projectId: proj.id, userId: colaborador.id, role: 'COLABORADOR' },
|
||||
});
|
||||
}
|
||||
|
||||
// Sample tasks for ERP project
|
||||
const task1 = await prisma.task.create({
|
||||
data: {
|
||||
projectId: proj3.id,
|
||||
title: 'Análisis de requerimientos',
|
||||
description: 'Levantamiento y documentación de todos los requerimientos del negocio',
|
||||
createdById: gerente.id,
|
||||
assigneeId: colaborador.id,
|
||||
status: 'COMPLETADA',
|
||||
priority: 'CRITICO',
|
||||
estimatedHours: 80,
|
||||
actualHours: 92,
|
||||
progressPct: 100,
|
||||
startDate: new Date('2026-02-01'),
|
||||
dueDate: new Date('2026-02-28'),
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.task.create({
|
||||
data: {
|
||||
projectId: proj3.id,
|
||||
parentId: task1.id,
|
||||
title: 'Entrevistas con stakeholders',
|
||||
createdById: gerente.id,
|
||||
assigneeId: colaborador.id,
|
||||
status: 'COMPLETADA',
|
||||
priority: 'ALTO',
|
||||
estimatedHours: 20,
|
||||
progressPct: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.task.create({
|
||||
data: {
|
||||
projectId: proj3.id,
|
||||
title: 'Desarrollo módulo de inventarios',
|
||||
description: 'Implementación del módulo de gestión de inventarios con integración WMS',
|
||||
createdById: gerente.id,
|
||||
assigneeId: colaborador.id,
|
||||
status: 'EN_PROCESO',
|
||||
priority: 'CRITICO',
|
||||
estimatedHours: 200,
|
||||
actualHours: 120,
|
||||
progressPct: 55,
|
||||
startDate: new Date('2026-03-15'),
|
||||
dueDate: new Date('2026-06-30'),
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Seed completado:');
|
||||
console.log(' Usuarios: admin@arpm.com, director@arpm.com, gerente@arpm.com, colaborador@arpm.com');
|
||||
console.log(' Password para todos: Admin123!');
|
||||
console.log(' Proyectos: PRY-2026-001, PRY-2026-002, PRY-2026-003');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(async () => { await prisma.$disconnect(); });
|
||||
@@ -0,0 +1,154 @@
|
||||
const bcrypt = require("bcryptjs");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { validationResult } = require("express-validator");
|
||||
const prisma = require("../models/prisma");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const generateTokens = (userId, role) => {
|
||||
const accessToken = jwt.sign(
|
||||
{ userId, role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_ACCESS_EXPIRES || "15m" }
|
||||
);
|
||||
const refreshToken = jwt.sign(
|
||||
{ userId },
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
{ expiresIn: process.env.JWT_REFRESH_EXPIRES || "7d" }
|
||||
);
|
||||
return { accessToken, refreshToken };
|
||||
};
|
||||
|
||||
const login = async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { email: email.toLowerCase() } });
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return res.status(401).json({ error: "Credenciales invalidas" });
|
||||
}
|
||||
|
||||
// Verificar si esta bloqueado
|
||||
if (user.lockedUntil && user.lockedUntil > new Date()) {
|
||||
const minutesLeft = Math.ceil((user.lockedUntil - new Date()) / 60000);
|
||||
return res.status(423).json({ error: "Cuenta bloqueada. Intenta en " + minutesLeft + " minutos." });
|
||||
}
|
||||
|
||||
const passwordValid = await bcrypt.compare(password, user.passwordHash);
|
||||
|
||||
if (!passwordValid) {
|
||||
const maxAttempts = parseInt(process.env.MAX_LOGIN_ATTEMPTS) || 5;
|
||||
const newAttempts = user.loginAttempts + 1;
|
||||
const lockData = newAttempts >= maxAttempts
|
||||
? { loginAttempts: newAttempts, lockedUntil: new Date(Date.now() + parseInt(process.env.LOCK_TIME_MINUTES || 15) * 60000) }
|
||||
: { loginAttempts: newAttempts };
|
||||
|
||||
await prisma.user.update({ where: { id: user.id }, data: lockData });
|
||||
return res.status(401).json({ error: "Credenciales invalidas" });
|
||||
}
|
||||
|
||||
// Reset intentos fallidos
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { loginAttempts: 0, lockedUntil: null, lastLoginAt: new Date(), lastLoginIp: req.ip },
|
||||
});
|
||||
|
||||
const { accessToken, refreshToken } = generateTokens(user.id, user.role);
|
||||
|
||||
// Guardar refresh token en BD
|
||||
await prisma.refreshToken.create({
|
||||
data: {
|
||||
token: refreshToken,
|
||||
userId: user.id,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.headers["user-agent"],
|
||||
},
|
||||
});
|
||||
|
||||
// Audit log
|
||||
await prisma.auditLog.create({
|
||||
data: { userId: user.id, action: "LOGIN", entity: "users", entityId: user.id, ipAddress: req.ip },
|
||||
});
|
||||
|
||||
res.json({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, role: user.role },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error en login:", error);
|
||||
res.status(500).json({ error: "Error interno del servidor" });
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
if (!refreshToken) return res.status(400).json({ error: "Refresh token requerido" });
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
||||
|
||||
const storedToken = await prisma.refreshToken.findUnique({
|
||||
where: { token: refreshToken },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (!storedToken || storedToken.revokedAt || storedToken.expiresAt < new Date()) {
|
||||
return res.status(401).json({ error: "Refresh token invalido o expirado" });
|
||||
}
|
||||
|
||||
if (!storedToken.user.isActive) {
|
||||
return res.status(401).json({ error: "Usuario inactivo" });
|
||||
}
|
||||
|
||||
// Revocar token antiguo y emitir nuevos
|
||||
await prisma.refreshToken.update({ where: { id: storedToken.id }, data: { revokedAt: new Date() } });
|
||||
|
||||
const tokens = generateTokens(storedToken.user.id, storedToken.user.role);
|
||||
|
||||
await prisma.refreshToken.create({
|
||||
data: {
|
||||
token: tokens.refreshToken,
|
||||
userId: storedToken.user.id,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.headers["user-agent"],
|
||||
},
|
||||
});
|
||||
|
||||
res.json(tokens);
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: "Refresh token invalido" });
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
|
||||
if (refreshToken) {
|
||||
await prisma.refreshToken.updateMany({
|
||||
where: { token: refreshToken, userId: req.user.id },
|
||||
data: { revokedAt: new Date() },
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: { userId: req.user.id, action: "LOGOUT", entity: "users", entityId: req.user.id, ipAddress: req.ip },
|
||||
}).catch(() => {});
|
||||
|
||||
res.json({ message: "Sesion cerrada exitosamente" });
|
||||
};
|
||||
|
||||
const me = async (req, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user.id },
|
||||
select: { id: true, email: true, firstName: true, lastName: true, role: true, isActive: true, phone: true, department: true, jobTitle: true, avatarUrl: true, lastLoginAt: true, createdAt: true },
|
||||
});
|
||||
res.json(user);
|
||||
};
|
||||
|
||||
module.exports = { login, refresh, logout, me };
|
||||
@@ -0,0 +1,57 @@
|
||||
const prisma = require("../models/prisma");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const getGlobalKPIs = async (req, res) => {
|
||||
try {
|
||||
const [
|
||||
totalProjects,
|
||||
projectsByStatus,
|
||||
recentProjects,
|
||||
totalTasks,
|
||||
tasksByStatus,
|
||||
totalUsers,
|
||||
] = await Promise.all([
|
||||
prisma.project.count(),
|
||||
prisma.project.groupBy({ by: ["status"], _count: true }),
|
||||
prisma.project.findMany({
|
||||
take: 5,
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { id: true, name: true, code: true, status: true, ragStatus: true, progressPct: true, budget: true, priority: true },
|
||||
}),
|
||||
prisma.task.count(),
|
||||
prisma.task.groupBy({ by: ["status"], _count: true }),
|
||||
prisma.user.count({ where: { isActive: true } }),
|
||||
]);
|
||||
|
||||
const statusMap = {};
|
||||
projectsByStatus.forEach((s) => { statusMap[s.status] = s._count; });
|
||||
|
||||
const taskStatusMap = {};
|
||||
tasksByStatus.forEach((s) => { taskStatusMap[s.status] = s._count; });
|
||||
|
||||
res.json({
|
||||
projects: {
|
||||
total: totalProjects,
|
||||
active: statusMap["ACTIVO"] || 0,
|
||||
paused: statusMap["PAUSADO"] || 0,
|
||||
completed: statusMap["COMPLETADO"] || 0,
|
||||
atRisk: statusMap["EN_RIESGO"] || 0,
|
||||
cancelled: statusMap["CANCELADO"] || 0,
|
||||
},
|
||||
tasks: {
|
||||
total: totalTasks,
|
||||
pending: taskStatusMap["PENDIENTE"] || 0,
|
||||
inProgress: taskStatusMap["EN_PROCESO"] || 0,
|
||||
blocked: taskStatusMap["BLOQUEADA"] || 0,
|
||||
completed: taskStatusMap["COMPLETADA"] || 0,
|
||||
},
|
||||
users: { active: totalUsers },
|
||||
recentProjects,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error obteniendo KPIs globales:", error);
|
||||
res.status(500).json({ error: "Error obteniendo KPIs" });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getGlobalKPIs };
|
||||
@@ -0,0 +1,114 @@
|
||||
const { validationResult } = require("express-validator");
|
||||
const prisma = require("../models/prisma");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const generateProjectCode = async () => {
|
||||
const year = new Date().getFullYear();
|
||||
const count = await prisma.project.count();
|
||||
return "PRY-" + year + "-" + String(count + 1).padStart(3, "0");
|
||||
};
|
||||
|
||||
const getAll = async (req, res) => {
|
||||
try {
|
||||
const { status, category, search, page = 1, limit = 20 } = req.query;
|
||||
const where = {};
|
||||
if (status) where.status = status;
|
||||
if (category) where.category = category;
|
||||
if (search) where.OR = [{ name: { contains: search, mode: "insensitive" } }, { code: { contains: search, mode: "insensitive" } }];
|
||||
|
||||
// COLABORADOR solo ve proyectos donde es miembro
|
||||
if (req.user.role === "COLABORADOR" || req.user.role === "JEFE_AREA") {
|
||||
where.members = { some: { userId: req.user.id } };
|
||||
}
|
||||
|
||||
const [projects, total] = await Promise.all([
|
||||
prisma.project.findMany({
|
||||
where,
|
||||
skip: (parseInt(page) - 1) * parseInt(limit),
|
||||
take: parseInt(limit),
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: {
|
||||
members: { include: { user: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } } } },
|
||||
_count: { select: { tasks: true } },
|
||||
},
|
||||
}),
|
||||
prisma.project.count({ where }),
|
||||
]);
|
||||
|
||||
res.json({ data: projects, total, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(total / parseInt(limit)) });
|
||||
} catch (error) {
|
||||
logger.error("Error obteniendo proyectos:", error);
|
||||
res.status(500).json({ error: "Error obteniendo proyectos" });
|
||||
}
|
||||
};
|
||||
|
||||
const getById = async (req, res) => {
|
||||
try {
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id: req.params.id },
|
||||
include: {
|
||||
members: { include: { user: { select: { id: true, firstName: true, lastName: true, role: true, avatarUrl: true } } } },
|
||||
tasks: { where: { parentId: null }, include: { children: true, assignee: { select: { id: true, firstName: true, lastName: true } } } },
|
||||
},
|
||||
});
|
||||
if (!project) return res.status(404).json({ error: "Proyecto no encontrado" });
|
||||
res.json(project);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Error obteniendo proyecto" });
|
||||
}
|
||||
};
|
||||
|
||||
const ALLOWED_PROJECT_CREATE_FIELDS = [
|
||||
'name', 'description', 'type', 'category', 'client',
|
||||
'startDate', 'plannedEndDate', 'budget', 'priority',
|
||||
'status', 'currentPhase', 'methodology', 'tags',
|
||||
];
|
||||
|
||||
const ALLOWED_PROJECT_UPDATE_FIELDS = [
|
||||
'name', 'description', 'type', 'category', 'client',
|
||||
'startDate', 'plannedEndDate', 'budget', 'priority',
|
||||
'status', 'currentPhase', 'methodology', 'ragStatus',
|
||||
'progressPct', 'tags',
|
||||
];
|
||||
|
||||
const pickFields = (obj, allowed) =>
|
||||
Object.fromEntries(Object.entries(obj).filter(([k]) => allowed.includes(k)));
|
||||
|
||||
const create = async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
|
||||
|
||||
try {
|
||||
const code = await generateProjectCode();
|
||||
const safeData = pickFields(req.body, ALLOWED_PROJECT_CREATE_FIELDS);
|
||||
const project = await prisma.project.create({
|
||||
data: { ...safeData, code, createdBy: req.user.id },
|
||||
});
|
||||
|
||||
// Agregar al creador como miembro GERENTE por defecto
|
||||
await prisma.projectMember.create({
|
||||
data: { projectId: project.id, userId: req.user.id, role: "GERENTE" },
|
||||
});
|
||||
|
||||
res.status(201).json(project);
|
||||
} catch (error) {
|
||||
logger.error("Error creando proyecto:", error);
|
||||
res.status(500).json({ error: "Error creando proyecto" });
|
||||
}
|
||||
};
|
||||
|
||||
const update = async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
|
||||
|
||||
try {
|
||||
const safeData = pickFields(req.body, ALLOWED_PROJECT_UPDATE_FIELDS);
|
||||
const project = await prisma.project.update({ where: { id: req.params.id }, data: safeData });
|
||||
res.json(project);
|
||||
} catch (error) {
|
||||
if (error.code === "P2025") return res.status(404).json({ error: "Proyecto no encontrado" });
|
||||
res.status(500).json({ error: "Error actualizando proyecto" });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getAll, getById, create, update };
|
||||
@@ -0,0 +1,124 @@
|
||||
const prisma = require('../models/prisma');
|
||||
const { validationResult } = require('express-validator');
|
||||
const { ROLES } = require('../utils/roles');
|
||||
|
||||
exports.getByProject = async (req, res, next) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const { status, assigneeId, parentId } = req.query;
|
||||
|
||||
const where = {
|
||||
projectId,
|
||||
parentId: parentId === 'null' ? null : parentId || undefined,
|
||||
};
|
||||
if (status) where.status = status;
|
||||
if (assigneeId) where.assigneeId = assigneeId;
|
||||
|
||||
const tasks = await prisma.task.findMany({
|
||||
where,
|
||||
include: {
|
||||
assignee: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
|
||||
creator: { select: { id: true, firstName: true, lastName: true } },
|
||||
_count: { select: { children: true, comments: true } },
|
||||
},
|
||||
orderBy: [{ priority: 'asc' }, { dueDate: 'asc' }],
|
||||
});
|
||||
|
||||
res.json({ tasks });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getById = async (req, res, next) => {
|
||||
try {
|
||||
const task = await prisma.task.findUniqueOrThrow({
|
||||
where: { id: req.params.id },
|
||||
include: {
|
||||
assignee: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
|
||||
creator: { select: { id: true, firstName: true, lastName: true } },
|
||||
children: {
|
||||
include: {
|
||||
assignee: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
include: { user: { select: { id: true, firstName: true, lastName: true } } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
res.json({ task });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
exports.create = async (req, res, next) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
|
||||
|
||||
const task = await prisma.task.create({
|
||||
data: { ...req.body, createdById: req.user.id },
|
||||
});
|
||||
res.status(201).json({ task });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
const ALLOWED_TASK_UPDATE_FIELDS = [
|
||||
'title', 'description', 'status', 'priority', 'assigneeId',
|
||||
'startDate', 'dueDate', 'estimatedHours', 'actualHours',
|
||||
'progressPct', 'parentId', 'tags',
|
||||
];
|
||||
|
||||
const pickTaskFields = (obj) =>
|
||||
Object.fromEntries(Object.entries(obj).filter(([k]) => ALLOWED_TASK_UPDATE_FIELDS.includes(k)));
|
||||
|
||||
exports.update = async (req, res, next) => {
|
||||
try {
|
||||
const safeData = pickTaskFields(req.body);
|
||||
const task = await prisma.task.update({
|
||||
where: { id: req.params.id },
|
||||
data: safeData,
|
||||
});
|
||||
res.json({ task });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
if (!status) return res.status(400).json({ message: 'status requerido' });
|
||||
|
||||
const task = await prisma.task.update({
|
||||
where: { id: req.params.id },
|
||||
data: { status },
|
||||
include: {
|
||||
assignee: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
res.json({ task });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
exports.addComment = async (req, res, next) => {
|
||||
try {
|
||||
const { content } = req.body;
|
||||
if (!content?.trim()) return res.status(400).json({ message: 'Contenido requerido' });
|
||||
|
||||
const comment = await prisma.comment.create({
|
||||
data: { taskId: req.params.id, userId: req.user.id, content: content.trim() },
|
||||
include: { user: { select: { id: true, firstName: true, lastName: true } } },
|
||||
});
|
||||
res.status(201).json({ comment });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
const prisma = require('../models/prisma');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { validationResult } = require('express-validator');
|
||||
|
||||
exports.getAll = async (req, res, next) => {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { isActive: true },
|
||||
select: { id: true, email: true, firstName: true, lastName: true, role: true, department: true, jobTitle: true, avatarUrl: true, lastLoginAt: true },
|
||||
orderBy: { firstName: 'asc' },
|
||||
});
|
||||
res.json({ users });
|
||||
} catch (err) { next(err); }
|
||||
};
|
||||
|
||||
exports.getById = async (req, res, next) => {
|
||||
try {
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: { id: req.params.id },
|
||||
select: { id: true, email: true, firstName: true, lastName: true, role: true, department: true, jobTitle: true, phone: true, avatarUrl: true, isActive: true, lastLoginAt: true, createdAt: true },
|
||||
});
|
||||
res.json({ user });
|
||||
} catch (err) { next(err); }
|
||||
};
|
||||
|
||||
exports.create = async (req, res, next) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
|
||||
const { password, ...rest } = req.body;
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
const user = await prisma.user.create({
|
||||
data: { ...rest, passwordHash },
|
||||
select: { id: true, email: true, firstName: true, lastName: true, role: true },
|
||||
});
|
||||
res.status(201).json({ user });
|
||||
} catch (err) { next(err); }
|
||||
};
|
||||
|
||||
exports.update = async (req, res, next) => {
|
||||
try {
|
||||
const { password, ...rest } = req.body;
|
||||
const data = { ...rest };
|
||||
if (password) data.passwordHash = await bcrypt.hash(password, 12);
|
||||
const user = await prisma.user.update({
|
||||
where: { id: req.params.id },
|
||||
data,
|
||||
select: { id: true, email: true, firstName: true, lastName: true, role: true, department: true, jobTitle: true },
|
||||
});
|
||||
res.json({ user });
|
||||
} catch (err) { next(err); }
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const cors = require('cors');
|
||||
const morgan = require('morgan');
|
||||
const { apiLimiter } = require('./middleware/rateLimiter.middleware');
|
||||
const { errorHandler, notFound } = require('./middleware/error.middleware');
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
const authRoutes = require('./routes/auth.routes');
|
||||
const dashboardRoutes = require('./routes/dashboard.routes');
|
||||
const projectsRoutes = require('./routes/projects.routes');
|
||||
const tasksRoutes = require('./routes/tasks.routes');
|
||||
const usersRoutes = require('./routes/users.routes');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 4000;
|
||||
|
||||
// Security
|
||||
app.use(helmet());
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Logging
|
||||
app.use(morgan('combined', {
|
||||
stream: { write: (msg) => logger.info(msg.trim()) },
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
app.use('/api', apiLimiter);
|
||||
|
||||
// Health check (no auth)
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/dashboard', dashboardRoutes);
|
||||
app.use('/api/projects', projectsRoutes);
|
||||
app.use('/api/projects/:projectId/tasks', tasksRoutes);
|
||||
app.use('/api/tasks', tasksRoutes);
|
||||
app.use('/api/users', usersRoutes);
|
||||
|
||||
// Error handling
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
logger.info(`AR-ProjectManagement backend running on port ${PORT}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@@ -0,0 +1,31 @@
|
||||
const prisma = require("../models/prisma");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const auditLog = (action, entity) => {
|
||||
return async (req, res, next) => {
|
||||
const originalJson = res.json.bind(res);
|
||||
res.json = async (data) => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try {
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: req.user?.id || null,
|
||||
action,
|
||||
entity,
|
||||
entityId: req.params?.id || data?.id || null,
|
||||
newValues: data || null,
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.headers["user-agent"],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Error guardando audit log:", err);
|
||||
}
|
||||
}
|
||||
return originalJson(data);
|
||||
};
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { auditLog };
|
||||
@@ -0,0 +1,59 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
const prisma = require("../models/prisma");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const authenticate = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Token de acceso requerido" });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
select: { id: true, email: true, firstName: true, lastName: true, role: true, isActive: true },
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return res.status(401).json({ error: "Usuario no encontrado o inactivo" });
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error.name === "TokenExpiredError") {
|
||||
return res.status(401).json({ error: "Token expirado", code: "TOKEN_EXPIRED" });
|
||||
}
|
||||
if (error.name === "JsonWebTokenError") {
|
||||
return res.status(401).json({ error: "Token invalido" });
|
||||
}
|
||||
logger.error("Error en middleware de autenticacion:", error);
|
||||
res.status(500).json({ error: "Error interno de autenticacion" });
|
||||
}
|
||||
};
|
||||
|
||||
const authorize = (...allowedRoles) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) return res.status(401).json({ error: "No autenticado" });
|
||||
if (!allowedRoles.includes(req.user.role)) {
|
||||
return res.status(403).json({
|
||||
error: "No tienes permisos para realizar esta accion",
|
||||
requiredRoles: allowedRoles,
|
||||
userRole: req.user.role,
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
const readOnlyForAuditor = (req, res, next) => {
|
||||
if (req.user?.role === "AUDITOR" && req.method !== "GET") {
|
||||
return res.status(403).json({ error: "Los auditores solo tienen acceso de lectura" });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = { authenticate, authorize, readOnlyForAuditor };
|
||||
@@ -0,0 +1,17 @@
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
logger.error({ message: err.message, url: req.url, method: req.method, userId: req.user?.id });
|
||||
|
||||
if (err.code === "P2002") return res.status(409).json({ error: "Valor duplicado", field: err.meta?.target });
|
||||
if (err.code === "P2025") return res.status(404).json({ error: "Registro no encontrado" });
|
||||
|
||||
const status = err.status || err.statusCode || 500;
|
||||
res.status(status).json({ error: status < 500 ? err.message : "Error interno del servidor" });
|
||||
};
|
||||
|
||||
const notFound = (_req, res) => {
|
||||
res.status(404).json({ error: "Ruta no encontrada" });
|
||||
};
|
||||
|
||||
module.exports = { errorHandler, notFound };
|
||||
@@ -0,0 +1,19 @@
|
||||
const rateLimit = require("express-rate-limit");
|
||||
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: parseInt(process.env.MAX_LOGIN_ATTEMPTS) || 5,
|
||||
message: { error: "Demasiados intentos de login. Intenta en 15 minutos." },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 100,
|
||||
message: { error: "Demasiadas solicitudes." },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
module.exports = { loginLimiter, apiLimiter };
|
||||
@@ -0,0 +1,22 @@
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
log: [
|
||||
{ emit: "event", level: "query" },
|
||||
{ emit: "event", level: "error" },
|
||||
{ emit: "event", level: "warn" },
|
||||
],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
prisma.$on("query", (e) => {
|
||||
logger.debug(`Query: ${e.query} | Duration: ${e.duration}ms`);
|
||||
});
|
||||
}
|
||||
|
||||
prisma.$on("error", (e) => {
|
||||
logger.error(`Prisma error: ${e.message}`);
|
||||
});
|
||||
|
||||
module.exports = prisma;
|
||||
@@ -0,0 +1,18 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { body } = require('express-validator');
|
||||
const authController = require('../controllers/auth.controller');
|
||||
const { authenticate } = require('../middleware/auth.middleware');
|
||||
const { loginLimiter } = require('../middleware/rateLimiter.middleware');
|
||||
|
||||
const loginValidation = [
|
||||
body('email').isEmail().normalizeEmail(),
|
||||
body('password').notEmpty().isLength({ min: 6 }),
|
||||
];
|
||||
|
||||
router.post('/login', loginLimiter, loginValidation, authController.login);
|
||||
router.post('/refresh', authController.refresh);
|
||||
router.post('/logout', authenticate, authController.logout);
|
||||
router.get('/me', authenticate, authController.me);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,8 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const dashboardController = require('../controllers/dashboard.controller');
|
||||
const { authenticate } = require('../middleware/auth.middleware');
|
||||
|
||||
router.get('/kpis', authenticate, dashboardController.getGlobalKPIs);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,35 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { body } = require('express-validator');
|
||||
const projectsController = require('../controllers/projects.controller');
|
||||
const { authenticate, authorize, readOnlyForAuditor } = require('../middleware/auth.middleware');
|
||||
const { ROLES } = require('../utils/roles');
|
||||
|
||||
const projectValidation = [
|
||||
body('name').trim().notEmpty().isLength({ max: 200 }),
|
||||
body('type').trim().notEmpty(),
|
||||
body('category').trim().notEmpty(),
|
||||
body('startDate').isISO8601(),
|
||||
body('plannedEndDate').isISO8601(),
|
||||
body('budget').isDecimal({ decimal_digits: '0,2' }),
|
||||
];
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/', projectsController.getAll);
|
||||
router.get('/:id', projectsController.getById);
|
||||
router.post(
|
||||
'/',
|
||||
authorize(ROLES.SUPER_ADMIN, ROLES.DIRECTOR, ROLES.GERENTE),
|
||||
readOnlyForAuditor,
|
||||
projectValidation,
|
||||
projectsController.create
|
||||
);
|
||||
router.patch(
|
||||
'/:id',
|
||||
authorize(ROLES.SUPER_ADMIN, ROLES.DIRECTOR, ROLES.GERENTE),
|
||||
readOnlyForAuditor,
|
||||
projectsController.update
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,13 @@
|
||||
const express = require('express');
|
||||
const router = express.Router({ mergeParams: true });
|
||||
const tasksController = require('../controllers/tasks.controller');
|
||||
const { authenticate, readOnlyForAuditor } = require('../middleware/auth.middleware');
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', tasksController.getByProject);
|
||||
router.get('/:id', tasksController.getById);
|
||||
router.post('/', readOnlyForAuditor, tasksController.create);
|
||||
router.patch('/:id', readOnlyForAuditor, tasksController.update);
|
||||
router.patch('/:id/status', readOnlyForAuditor, tasksController.updateStatus);
|
||||
router.post('/:id/comments', tasksController.addComment);
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { body } = require('express-validator');
|
||||
const usersController = require('../controllers/users.controller');
|
||||
const { authenticate, authorize } = require('../middleware/auth.middleware');
|
||||
const { ROLES } = require('../utils/roles');
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', usersController.getAll);
|
||||
router.get('/:id', usersController.getById);
|
||||
router.post('/', authorize(ROLES.SUPER_ADMIN, ROLES.DIRECTOR), [
|
||||
body('email').isEmail().normalizeEmail(),
|
||||
body('password').isLength({ min: 8 }),
|
||||
body('firstName').trim().notEmpty(),
|
||||
body('lastName').trim().notEmpty(),
|
||||
body('role').isIn(Object.values(ROLES)),
|
||||
], usersController.create);
|
||||
router.patch('/:id', authorize(ROLES.SUPER_ADMIN, ROLES.DIRECTOR), usersController.update);
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,30 @@
|
||||
const winston = require("winston");
|
||||
const path = require("path");
|
||||
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
),
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: path.join(__dirname, "../../logs/error.log"),
|
||||
level: "error",
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: path.join(__dirname, "../../logs/combined.log"),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
@@ -0,0 +1,18 @@
|
||||
const ROLES = {
|
||||
SUPER_ADMIN: "SUPER_ADMIN",
|
||||
DIRECTOR: "DIRECTOR",
|
||||
GERENTE: "GERENTE",
|
||||
JEFE_AREA: "JEFE_AREA",
|
||||
COLABORADOR: "COLABORADOR",
|
||||
FINANCIERO: "FINANCIERO",
|
||||
AUDITOR: "AUDITOR",
|
||||
};
|
||||
|
||||
const ROLE_GROUPS = {
|
||||
MANAGERS: ["SUPER_ADMIN", "DIRECTOR", "GERENTE"],
|
||||
FINANCE: ["SUPER_ADMIN", "DIRECTOR", "FINANCIERO"],
|
||||
APPROVERS: ["SUPER_ADMIN", "DIRECTOR"],
|
||||
ALL_STAFF: Object.values(ROLES),
|
||||
};
|
||||
|
||||
module.exports = { ROLES, ROLE_GROUPS };
|
||||
Reference in New Issue
Block a user