@gzl10/nexus-backend 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2267 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/modules/system/system.controller.ts
12
+ function toModuleDTO(mod) {
13
+ return {
14
+ name: mod.name,
15
+ code: mod.code,
16
+ label: mod.label,
17
+ icon: mod.icon,
18
+ description: mod.description,
19
+ version: mod.version,
20
+ type: mod.type ?? "core",
21
+ category: mod.category ?? "",
22
+ dependencies: mod.dependencies ?? [],
23
+ routePrefix: mod.routePrefix ?? `/${mod.name}`,
24
+ subjects: mod.subjects ?? [],
25
+ entities: mod.entities ?? [],
26
+ hasRoutes: !!mod.routes,
27
+ hasMigrate: !!mod.migrate,
28
+ hasSeed: !!mod.seed,
29
+ hasInit: !!mod.init
30
+ };
31
+ }
32
+ function createSystemController(ctx) {
33
+ const { errors } = ctx;
34
+ return {
35
+ /**
36
+ * GET /system/modules
37
+ * Lista todos los módulos registrados
38
+ */
39
+ listModules(_req, res) {
40
+ const modules2 = getModules().map(toModuleDTO);
41
+ res.json({ data: modules2, total: modules2.length });
42
+ },
43
+ /**
44
+ * GET /system/modules/:code
45
+ * Obtiene un módulo por código
46
+ */
47
+ getModule(req, res) {
48
+ const { code } = req.params;
49
+ const mod = getModules().find((m) => m.code === code.toUpperCase());
50
+ if (!mod) {
51
+ throw new errors.NotFoundError("M\xF3dulo no encontrado");
52
+ }
53
+ res.json({ data: toModuleDTO(mod) });
54
+ }
55
+ };
56
+ }
57
+ var init_system_controller = __esm({
58
+ "src/modules/system/system.controller.ts"() {
59
+ "use strict";
60
+ init_modules();
61
+ }
62
+ });
63
+
64
+ // src/modules/system/system.routes.ts
65
+ function createSystemRoutes(ctx) {
66
+ const router = ctx.createRouter();
67
+ const controller = createSystemController(ctx);
68
+ router.get("/modules", controller.listModules);
69
+ router.get("/modules/:code", controller.getModule);
70
+ return router;
71
+ }
72
+ var init_system_routes = __esm({
73
+ "src/modules/system/system.routes.ts"() {
74
+ "use strict";
75
+ init_system_controller();
76
+ }
77
+ });
78
+
79
+ // src/modules/system/index.ts
80
+ var systemModule;
81
+ var init_system = __esm({
82
+ "src/modules/system/index.ts"() {
83
+ "use strict";
84
+ init_system_routes();
85
+ systemModule = {
86
+ name: "system",
87
+ code: "SYS",
88
+ label: "System",
89
+ icon: "mdi:cog-outline",
90
+ description: "System configuration, module registry, and platform metadata",
91
+ type: "core",
92
+ dependencies: [],
93
+ routes: createSystemRoutes,
94
+ routePrefix: "/system",
95
+ subjects: []
96
+ };
97
+ }
98
+ });
99
+
100
+ // src/modules/roles/roles.migrate.ts
101
+ async function migrate(ctx) {
102
+ const { db: db2, logger: logger2, helpers } = ctx;
103
+ const { addTimestamps: addTimestamps2, addAuditFieldsIfMissing: addAuditFieldsIfMissing2 } = helpers;
104
+ if (!await db2.schema.hasTable("rol_roles")) {
105
+ await db2.schema.createTable("rol_roles", (table) => {
106
+ table.string("id").primary();
107
+ table.string("name", 50).unique().notNullable();
108
+ table.string("description", 255);
109
+ table.boolean("is_system").defaultTo(false);
110
+ addTimestamps2(table, db2);
111
+ });
112
+ logger2.info("Created table: rol_roles");
113
+ }
114
+ if (!await db2.schema.hasTable("rol_role_permissions")) {
115
+ await db2.schema.createTable("rol_role_permissions", (table) => {
116
+ table.string("id").primary();
117
+ table.string("role_id").notNullable().references("id").inTable("rol_roles").onDelete("CASCADE");
118
+ table.string("action", 20).notNullable();
119
+ table.string("subject", 50).notNullable();
120
+ table.json("conditions");
121
+ table.json("fields");
122
+ table.boolean("inverted").defaultTo(false);
123
+ addTimestamps2(table, db2);
124
+ table.unique(["role_id", "action", "subject"]);
125
+ });
126
+ logger2.info("Created table: rol_role_permissions");
127
+ }
128
+ await addAuditFieldsIfMissing2(db2, "rol_roles");
129
+ await addAuditFieldsIfMissing2(db2, "rol_role_permissions");
130
+ }
131
+ var init_roles_migrate = __esm({
132
+ "src/modules/roles/roles.migrate.ts"() {
133
+ "use strict";
134
+ }
135
+ });
136
+
137
+ // src/modules/roles/roles.seed.ts
138
+ async function seed(ctx) {
139
+ const { db: db2, logger: logger2, generateId: generateId2 } = ctx;
140
+ const existingRoles = await db2("rol_roles").count("* as count").first();
141
+ if (Number(existingRoles?.count ?? 0) > 0) return;
142
+ const systemRoles = [
143
+ { id: generateId2(), name: "ADMIN", description: "Administrador con control total", is_system: true },
144
+ { id: generateId2(), name: "EDITOR", description: "Puede crear y editar contenido propio", is_system: true },
145
+ { id: generateId2(), name: "VIEWER", description: "Solo lectura", is_system: true }
146
+ ];
147
+ await db2("rol_roles").insert(systemRoles);
148
+ logger2.info("Inserted system roles: ADMIN, EDITOR, VIEWER");
149
+ const roles = await db2("rol_roles").select("id", "name");
150
+ const roleMap = Object.fromEntries(roles.map((r) => [r.name, r.id]));
151
+ const permissions = [
152
+ // ADMIN: manage all
153
+ { id: generateId2(), role_id: roleMap["ADMIN"], action: "manage", subject: "all", conditions: null, inverted: false },
154
+ // EDITOR: read users, CRUD posts (own for update/delete)
155
+ { id: generateId2(), role_id: roleMap["EDITOR"], action: "read", subject: "UsrUser", conditions: null, inverted: false },
156
+ { id: generateId2(), role_id: roleMap["EDITOR"], action: "create", subject: "PstPost", conditions: null, inverted: false },
157
+ { id: generateId2(), role_id: roleMap["EDITOR"], action: "read", subject: "PstPost", conditions: null, inverted: false },
158
+ { id: generateId2(), role_id: roleMap["EDITOR"], action: "update", subject: "PstPost", conditions: JSON.stringify({ author_id: "${user.id}" }), inverted: false },
159
+ { id: generateId2(), role_id: roleMap["EDITOR"], action: "delete", subject: "PstPost", conditions: JSON.stringify({ author_id: "${user.id}" }), inverted: false },
160
+ // VIEWER: read published posts, read own profile
161
+ { id: generateId2(), role_id: roleMap["VIEWER"], action: "read", subject: "PstPost", conditions: JSON.stringify({ status: "PUBLISHED" }), inverted: false },
162
+ { id: generateId2(), role_id: roleMap["VIEWER"], action: "read", subject: "UsrUser", conditions: JSON.stringify({ id: "${user.id}" }), inverted: false }
163
+ ];
164
+ await db2("rol_role_permissions").insert(permissions);
165
+ logger2.info("Inserted permissions for system roles");
166
+ }
167
+ var init_roles_seed = __esm({
168
+ "src/modules/roles/roles.seed.ts"() {
169
+ "use strict";
170
+ }
171
+ });
172
+
173
+ // src/modules/roles/roles.service.ts
174
+ function createRolesService(ctx) {
175
+ const { db: db2, errors, generateId: generateId2 } = ctx;
176
+ return {
177
+ async findAllPermissions(query) {
178
+ const { page, limit } = query;
179
+ const offset = (page - 1) * limit;
180
+ const baseQuery = db2("rol_role_permissions").select(
181
+ "rol_role_permissions.*",
182
+ "rol_roles.name as role_name"
183
+ ).leftJoin("rol_roles", "rol_role_permissions.role_id", "rol_roles.id");
184
+ const [permissions, countResult] = await Promise.all([
185
+ baseQuery.clone().orderBy("rol_roles.name", "asc").orderBy("subject", "asc").limit(limit).offset(offset),
186
+ db2("rol_role_permissions").count("* as count").first()
187
+ ]);
188
+ const total = Number(countResult?.count ?? 0);
189
+ const items = permissions.map((p) => ({
190
+ ...p,
191
+ conditions: typeof p.conditions === "string" ? JSON.parse(p.conditions) : p.conditions,
192
+ fields: typeof p.fields === "string" ? JSON.parse(p.fields) : p.fields,
193
+ role: { name: p.role_name }
194
+ }));
195
+ const totalPages = Math.ceil(total / limit);
196
+ return {
197
+ items,
198
+ total,
199
+ page,
200
+ limit,
201
+ totalPages,
202
+ hasNext: page < totalPages
203
+ };
204
+ },
205
+ async findAll(query) {
206
+ const { page, limit } = query;
207
+ const offset = (page - 1) * limit;
208
+ const permissionsCountSubquery = db2("rol_role_permissions").count("*").whereRaw("rol_role_permissions.role_id = rol_roles.id").as("permissions_count");
209
+ const usersCountSubquery = db2("usr_users").count("*").whereRaw("usr_users.role_id = rol_roles.id").as("users_count");
210
+ const baseQuery = db2("rol_roles").select("rol_roles.*", permissionsCountSubquery, usersCountSubquery);
211
+ const [roles, countResult] = await Promise.all([
212
+ baseQuery.clone().orderBy("name", "asc").limit(limit).offset(offset),
213
+ baseQuery.clone().count("* as count").first()
214
+ ]);
215
+ const total = Number(countResult?.count ?? 0);
216
+ const items = roles.map((role) => ({
217
+ ...role,
218
+ permissions_count: Number(role.permissions_count ?? 0),
219
+ users_count: Number(role.users_count ?? 0)
220
+ }));
221
+ const totalPages = Math.ceil(total / limit);
222
+ return {
223
+ items,
224
+ total,
225
+ page,
226
+ limit,
227
+ totalPages,
228
+ hasNext: page < totalPages
229
+ };
230
+ },
231
+ async findById(id) {
232
+ const role = await db2("rol_roles").where({ id }).first();
233
+ if (!role) throw new errors.NotFoundError("Rol");
234
+ const permissions = await db2("rol_role_permissions").where({ role_id: id }).orderBy("subject", "asc").orderBy("action", "asc");
235
+ const parsedPermissions = permissions.map((p) => ({
236
+ ...p,
237
+ conditions: typeof p.conditions === "string" ? JSON.parse(p.conditions) : p.conditions,
238
+ fields: typeof p.fields === "string" ? JSON.parse(p.fields) : p.fields
239
+ }));
240
+ return { ...role, permissions: parsedPermissions };
241
+ },
242
+ async findByName(name) {
243
+ return db2("rol_roles").where({ name }).first();
244
+ },
245
+ async getPermissionsByRoleId(roleId) {
246
+ const permissions = await db2("rol_role_permissions").where({ role_id: roleId });
247
+ return permissions.map((p) => ({
248
+ ...p,
249
+ conditions: typeof p.conditions === "string" ? JSON.parse(p.conditions) : p.conditions,
250
+ fields: typeof p.fields === "string" ? JSON.parse(p.fields) : p.fields
251
+ }));
252
+ },
253
+ async create(input, userId) {
254
+ return db2.transaction(async (trx) => {
255
+ const existing = await trx("rol_roles").where({ name: input.name }).first();
256
+ if (existing) {
257
+ throw new errors.ConflictError("Ya existe un rol con ese nombre");
258
+ }
259
+ const id = generateId2();
260
+ await trx("rol_roles").insert({
261
+ id,
262
+ name: input.name,
263
+ description: input.description || null,
264
+ is_system: false,
265
+ created_by: userId ?? null
266
+ });
267
+ const role = await trx("rol_roles").where({ id }).first();
268
+ return role;
269
+ });
270
+ },
271
+ async update(id, input, userId) {
272
+ return db2.transaction(async (trx) => {
273
+ const role = await trx("rol_roles").where({ id }).first();
274
+ if (!role) throw new errors.NotFoundError("Rol");
275
+ if (role.is_system) {
276
+ throw new errors.ForbiddenError("No se pueden modificar roles del sistema");
277
+ }
278
+ if (input.name && input.name !== role.name) {
279
+ const existing = await trx("rol_roles").where({ name: input.name }).first();
280
+ if (existing) {
281
+ throw new errors.ConflictError("Ya existe un rol con ese nombre");
282
+ }
283
+ }
284
+ await trx("rol_roles").where({ id }).update({
285
+ ...input,
286
+ updated_at: /* @__PURE__ */ new Date(),
287
+ updated_by: userId ?? null
288
+ });
289
+ const updated = await trx("rol_roles").where({ id }).first();
290
+ return updated;
291
+ });
292
+ },
293
+ async delete(id) {
294
+ const role = await db2("rol_roles").where({ id }).first();
295
+ if (!role) throw new errors.NotFoundError("Rol");
296
+ if (role.is_system) {
297
+ throw new errors.ForbiddenError("No se pueden eliminar roles del sistema");
298
+ }
299
+ const usersCount = await db2("usr_users").where({ role_id: id }).count("* as count").first();
300
+ if (Number(usersCount?.count ?? 0) > 0) {
301
+ throw new errors.ConflictError("No se puede eliminar un rol con usuarios asignados");
302
+ }
303
+ await db2("rol_roles").where({ id }).delete();
304
+ },
305
+ async updatePermissions(id, permissions, userId) {
306
+ return db2.transaction(async (trx) => {
307
+ const role = await trx("rol_roles").where({ id }).first();
308
+ if (!role) throw new errors.NotFoundError("Rol");
309
+ if (role.is_system && role.name === "ADMIN") {
310
+ throw new errors.ForbiddenError("No se pueden modificar los permisos del rol ADMIN");
311
+ }
312
+ await trx("rol_role_permissions").where({ role_id: id }).delete();
313
+ if (permissions.length > 0) {
314
+ const permissionsData = permissions.map((p) => ({
315
+ id: generateId2(),
316
+ role_id: id,
317
+ action: p.action,
318
+ subject: p.subject,
319
+ conditions: p.conditions ? JSON.stringify(p.conditions) : null,
320
+ fields: p.fields ? JSON.stringify(p.fields) : null,
321
+ inverted: p.inverted,
322
+ created_by: userId ?? null
323
+ }));
324
+ await trx("rol_role_permissions").insert(permissionsData);
325
+ }
326
+ await trx("rol_roles").where({ id }).update({
327
+ updated_at: /* @__PURE__ */ new Date(),
328
+ updated_by: userId ?? null
329
+ });
330
+ const updatedRole = await trx("rol_roles").where({ id }).first();
331
+ const updatedPermissions = await trx("rol_role_permissions").where({ role_id: id }).orderBy("subject", "asc").orderBy("action", "asc");
332
+ const parsedPermissions = updatedPermissions.map((p) => ({
333
+ ...p,
334
+ conditions: typeof p.conditions === "string" ? JSON.parse(p.conditions) : p.conditions,
335
+ fields: typeof p.fields === "string" ? JSON.parse(p.fields) : p.fields
336
+ }));
337
+ return { ...updatedRole, permissions: parsedPermissions };
338
+ });
339
+ }
340
+ };
341
+ }
342
+ var init_roles_service = __esm({
343
+ "src/modules/roles/roles.service.ts"() {
344
+ "use strict";
345
+ }
346
+ });
347
+
348
+ // src/modules/roles/roles.controller.ts
349
+ function createRolesController(ctx) {
350
+ const { ForbiddenError: CASLForbiddenError3 } = ctx.abilities;
351
+ const rolesService = createRolesService(ctx);
352
+ return {
353
+ async findAllPermissions(req, res) {
354
+ const authReq = req;
355
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("read", "RolRolePermission");
356
+ const result = await rolesService.findAllPermissions(req.validated?.query);
357
+ res.json({ success: true, data: result });
358
+ },
359
+ async findAll(req, res) {
360
+ const authReq = req;
361
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("read", "RolRole");
362
+ const result = await rolesService.findAll(req.validated?.query);
363
+ res.json({ success: true, data: result });
364
+ },
365
+ async findById(req, res) {
366
+ const authReq = req;
367
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("read", "RolRole");
368
+ const { id } = req.validated?.params;
369
+ const role = await rolesService.findById(id);
370
+ res.json({ success: true, data: role });
371
+ },
372
+ async create(req, res) {
373
+ const authReq = req;
374
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("create", "RolRole");
375
+ const role = await rolesService.create(req.body, authReq.user.id);
376
+ res.status(201).json({ success: true, data: role });
377
+ },
378
+ async update(req, res) {
379
+ const authReq = req;
380
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("update", "RolRole");
381
+ const { id } = req.validated?.params;
382
+ const role = await rolesService.update(id, req.body, authReq.user.id);
383
+ res.json({ success: true, data: role });
384
+ },
385
+ async delete(req, res) {
386
+ const authReq = req;
387
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("delete", "RolRole");
388
+ const { id } = req.validated?.params;
389
+ await rolesService.delete(id);
390
+ res.json({ success: true, message: "Rol eliminado" });
391
+ },
392
+ async updatePermissions(req, res) {
393
+ const authReq = req;
394
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("update", "RolRole");
395
+ const { id } = req.validated?.params;
396
+ const { permissions } = req.body;
397
+ const role = await rolesService.updatePermissions(id, permissions, authReq.user.id);
398
+ res.json({ success: true, data: role });
399
+ }
400
+ };
401
+ }
402
+ var init_roles_controller = __esm({
403
+ "src/modules/roles/roles.controller.ts"() {
404
+ "use strict";
405
+ init_roles_service();
406
+ }
407
+ });
408
+
409
+ // src/modules/roles/roles.schemas.ts
410
+ import { z } from "zod";
411
+ var createRoleSchema, updateRoleSchema, permissionSchema, updatePermissionsSchema, roleParamsSchema, roleQuerySchema;
412
+ var init_roles_schemas = __esm({
413
+ "src/modules/roles/roles.schemas.ts"() {
414
+ "use strict";
415
+ createRoleSchema = z.object({
416
+ name: z.string().min(2, "Nombre m\xEDnimo 2 caracteres").max(50, "Nombre m\xE1ximo 50 caracteres").regex(/^[A-Z_]+$/, "Solo may\xFAsculas y guion bajo"),
417
+ description: z.string().max(255).optional()
418
+ });
419
+ updateRoleSchema = z.object({
420
+ name: z.string().min(2, "Nombre m\xEDnimo 2 caracteres").max(50, "Nombre m\xE1ximo 50 caracteres").regex(/^[A-Z_]+$/, "Solo may\xFAsculas y guion bajo").optional(),
421
+ description: z.string().max(255).optional()
422
+ });
423
+ permissionSchema = z.object({
424
+ action: z.enum(["manage", "create", "read", "update", "delete"]),
425
+ subject: z.enum(["UsrUser", "PstPost", "RolRole", "RolRolePermission", "all"]),
426
+ conditions: z.record(z.unknown()).optional(),
427
+ fields: z.array(z.string()).optional(),
428
+ inverted: z.boolean().default(false)
429
+ });
430
+ updatePermissionsSchema = z.object({
431
+ permissions: z.array(permissionSchema)
432
+ });
433
+ roleParamsSchema = z.object({
434
+ id: z.string().min(1)
435
+ });
436
+ roleQuerySchema = z.object({
437
+ page: z.coerce.number().min(1).default(1),
438
+ limit: z.coerce.number().min(1).max(100).default(10)
439
+ });
440
+ }
441
+ });
442
+
443
+ // src/modules/roles/roles.routes.ts
444
+ function createRolesRoutes(ctx) {
445
+ const router = ctx.createRouter();
446
+ const { validate: validate2, auth } = ctx.middleware;
447
+ const controller = createRolesController(ctx);
448
+ router.use(auth);
449
+ router.get("/", validate2({ query: roleQuerySchema }), controller.findAll);
450
+ router.get("/permissions", validate2({ query: roleQuerySchema }), controller.findAllPermissions);
451
+ router.get("/:id", validate2({ params: roleParamsSchema }), controller.findById);
452
+ router.post("/", validate2({ body: createRoleSchema }), controller.create);
453
+ router.put("/:id", validate2({ params: roleParamsSchema, body: updateRoleSchema }), controller.update);
454
+ router.delete("/:id", validate2({ params: roleParamsSchema }), controller.delete);
455
+ router.put("/:id/permissions", validate2({ params: roleParamsSchema, body: updatePermissionsSchema }), controller.updatePermissions);
456
+ return router;
457
+ }
458
+ var init_roles_routes = __esm({
459
+ "src/modules/roles/roles.routes.ts"() {
460
+ "use strict";
461
+ init_roles_controller();
462
+ init_roles_schemas();
463
+ }
464
+ });
465
+
466
+ // src/modules/roles/index.ts
467
+ var rolesModule;
468
+ var init_roles = __esm({
469
+ "src/modules/roles/index.ts"() {
470
+ "use strict";
471
+ init_roles_migrate();
472
+ init_roles_seed();
473
+ init_roles_routes();
474
+ rolesModule = {
475
+ name: "roles",
476
+ code: "ROL",
477
+ label: "Roles",
478
+ icon: "mdi:shield-outline",
479
+ description: "Role definitions and CASL-based permission rules for access control",
480
+ type: "core",
481
+ dependencies: [],
482
+ migrate,
483
+ seed,
484
+ routes: createRolesRoutes,
485
+ routePrefix: "/roles",
486
+ subjects: ["RolRole", "RolRolePermission"],
487
+ entities: [
488
+ {
489
+ name: "RolRole",
490
+ label: "Roles",
491
+ listType: "list",
492
+ listFields: {
493
+ name: "Name",
494
+ description: "Description"
495
+ },
496
+ formFields: {
497
+ name: {
498
+ label: "Name",
499
+ type: "text",
500
+ required: true,
501
+ placeholder: "ADMIN_ROLE",
502
+ validation: {
503
+ minLength: 2,
504
+ maxLength: 50,
505
+ pattern: "^[A-Z][A-Z0-9_]*$",
506
+ errorMessage: "Use UPPER_SNAKE_CASE (e.g., ADMIN_ROLE)"
507
+ }
508
+ },
509
+ description: {
510
+ label: "Description",
511
+ type: "textarea",
512
+ placeholder: "Role description...",
513
+ validation: {
514
+ maxLength: 500
515
+ }
516
+ }
517
+ },
518
+ labelField: "name"
519
+ },
520
+ {
521
+ name: "RolRolePermission",
522
+ label: "Permissions",
523
+ routePrefix: "/roles/permissions",
524
+ listFields: {
525
+ action: "Action",
526
+ subject: "Subject",
527
+ "role.name": "Role"
528
+ },
529
+ labelField: "action"
530
+ }
531
+ ]
532
+ };
533
+ }
534
+ });
535
+
536
+ // src/modules/users/users.migrate.ts
537
+ async function migrate2(ctx) {
538
+ const { db: db2, logger: logger2, helpers } = ctx;
539
+ const { addTimestamps: addTimestamps2, addAuditFieldsIfMissing: addAuditFieldsIfMissing2, addColumnIfMissing: addColumnIfMissing2 } = helpers;
540
+ if (!await db2.schema.hasTable("usr_users")) {
541
+ await db2.schema.createTable("usr_users", (table) => {
542
+ table.string("id").primary();
543
+ table.string("email").unique().notNullable();
544
+ table.string("password").notNullable();
545
+ table.string("name").notNullable();
546
+ table.string("role_id").notNullable().references("id").inTable("rol_roles");
547
+ addTimestamps2(table, db2);
548
+ });
549
+ logger2.info("Created table: usr_users");
550
+ }
551
+ await migrateLegacyRoleColumn(ctx);
552
+ await addColumnIfMissing2(db2, "usr_users", "metadata", (table) => {
553
+ table.json("metadata").nullable();
554
+ });
555
+ await addAuditFieldsIfMissing2(db2, "usr_users");
556
+ }
557
+ async function migrateLegacyRoleColumn(ctx) {
558
+ const { db: db2, logger: logger2, dbType } = ctx;
559
+ const hasRoleColumn = await db2.schema.hasColumn("usr_users", "role");
560
+ const hasRoleIdColumn = await db2.schema.hasColumn("usr_users", "role_id");
561
+ if (!hasRoleColumn || hasRoleIdColumn) return;
562
+ logger2.info("Migrating usr_users.role to usr_users.role_id...");
563
+ const roles = await db2("rol_roles").select("id", "name");
564
+ const roleMap = Object.fromEntries(roles.map((r) => [r.name, r.id]));
565
+ await db2.schema.alterTable("usr_users", (table) => {
566
+ table.string("role_id").references("id").inTable("rol_roles");
567
+ });
568
+ for (const [roleName, roleId] of Object.entries(roleMap)) {
569
+ await db2("usr_users").where("role", roleName).update({ role_id: roleId });
570
+ }
571
+ if (dbType === "sqlite") {
572
+ await db2.raw(`
573
+ CREATE TABLE usr_users_new (
574
+ id TEXT PRIMARY KEY,
575
+ email TEXT UNIQUE NOT NULL,
576
+ password TEXT NOT NULL,
577
+ name TEXT NOT NULL,
578
+ role_id TEXT NOT NULL REFERENCES rol_roles(id),
579
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
580
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
581
+ )
582
+ `);
583
+ await db2.raw(`
584
+ INSERT INTO usr_users_new (id, email, password, name, role_id, created_at, updated_at)
585
+ SELECT id, email, password, name, role_id, created_at, updated_at FROM usr_users
586
+ `);
587
+ await db2.raw("DROP TABLE usr_users");
588
+ await db2.raw("ALTER TABLE usr_users_new RENAME TO usr_users");
589
+ } else {
590
+ await db2.schema.alterTable("usr_users", (table) => {
591
+ table.dropColumn("role");
592
+ });
593
+ }
594
+ logger2.info("Migration completed: usr_users.role -> usr_users.role_id");
595
+ }
596
+ var init_users_migrate = __esm({
597
+ "src/modules/users/users.migrate.ts"() {
598
+ "use strict";
599
+ }
600
+ });
601
+
602
+ // src/modules/auth/auth.utils.ts
603
+ import bcrypt from "bcryptjs";
604
+ function hashPassword(password) {
605
+ return bcrypt.hash(password, SALT_ROUNDS);
606
+ }
607
+ function verifyPassword(password, hash) {
608
+ return bcrypt.compare(password, hash);
609
+ }
610
+ var SALT_ROUNDS, DUMMY_HASH;
611
+ var init_auth_utils = __esm({
612
+ "src/modules/auth/auth.utils.ts"() {
613
+ "use strict";
614
+ SALT_ROUNDS = 10;
615
+ DUMMY_HASH = "$2a$10$N9qo8uLOickgx2ZMRZoMyeUv9rT7dRVqTMtKYzOYQVxZi1qU2cFaW";
616
+ }
617
+ });
618
+
619
+ // src/modules/users/users.seed.ts
620
+ async function seed2(ctx) {
621
+ const { db: db2, logger: logger2, generateId: generateId2 } = ctx;
622
+ const email = process.env["ADMIN_EMAIL"] || "admin@example.com";
623
+ const password = process.env["ADMIN_PASSWORD"] || "admin123";
624
+ const existing = await db2("usr_users").where({ email }).first();
625
+ if (existing) {
626
+ logger2.info({ email }, "Admin user already exists");
627
+ return;
628
+ }
629
+ const adminRole = await db2("rol_roles").where({ name: "ADMIN" }).first();
630
+ if (!adminRole) {
631
+ logger2.error("ADMIN role not found. Run migrations first.");
632
+ throw new Error("ADMIN role not found");
633
+ }
634
+ const hashedPassword = await hashPassword(password);
635
+ await db2("usr_users").insert({
636
+ id: generateId2(),
637
+ email,
638
+ password: hashedPassword,
639
+ name: "Administrador",
640
+ role_id: adminRole.id
641
+ });
642
+ logger2.info({ email }, "Admin user created");
643
+ }
644
+ var init_users_seed = __esm({
645
+ "src/modules/users/users.seed.ts"() {
646
+ "use strict";
647
+ init_auth_utils();
648
+ }
649
+ });
650
+
651
+ // src/modules/users/users.service.ts
652
+ function excludePassword(user) {
653
+ const { password: _, ...rest } = user;
654
+ return rest;
655
+ }
656
+ function buildUserWithRole(row) {
657
+ const {
658
+ password: _,
659
+ role_name,
660
+ role_description,
661
+ role_is_system,
662
+ role_created_at,
663
+ role_updated_at,
664
+ ...user
665
+ } = row;
666
+ return {
667
+ ...user,
668
+ role: user.role_id ? {
669
+ id: user.role_id,
670
+ name: role_name,
671
+ description: role_description,
672
+ is_system: role_is_system,
673
+ created_at: role_created_at,
674
+ updated_at: role_updated_at
675
+ } : null
676
+ };
677
+ }
678
+ function createUsersService(ctx) {
679
+ const { db: db2, errors, generateId: generateId2 } = ctx;
680
+ return {
681
+ async findAll(query) {
682
+ const { page, limit, role_id } = query;
683
+ const offset = (page - 1) * limit;
684
+ let baseQuery = db2("usr_users").select(...USER_WITH_ROLE_COLUMNS).leftJoin("rol_roles", "usr_users.role_id", "rol_roles.id");
685
+ if (role_id) {
686
+ baseQuery = baseQuery.where("usr_users.role_id", role_id);
687
+ }
688
+ const [users, countResult] = await Promise.all([
689
+ baseQuery.clone().orderBy("usr_users.created_at", "desc").limit(limit).offset(offset),
690
+ baseQuery.clone().count("* as count").first()
691
+ ]);
692
+ const total = Number(countResult?.count ?? 0);
693
+ const items = users.map(buildUserWithRole);
694
+ const totalPages = Math.ceil(total / limit);
695
+ return {
696
+ items,
697
+ total,
698
+ page,
699
+ limit,
700
+ totalPages,
701
+ hasNext: page < totalPages
702
+ };
703
+ },
704
+ async findById(id) {
705
+ const user = await db2("usr_users").where({ id }).first();
706
+ if (!user) throw new errors.NotFoundError("Usuario");
707
+ return excludePassword(user);
708
+ },
709
+ async findByIdWithRole(id) {
710
+ const row = await db2("usr_users").select(...USER_WITH_ROLE_COLUMNS).leftJoin("rol_roles", "usr_users.role_id", "rol_roles.id").where("usr_users.id", id).first();
711
+ if (!row) throw new errors.NotFoundError("Usuario");
712
+ return buildUserWithRole(row);
713
+ },
714
+ async findByIdWithPassword(id) {
715
+ const user = await db2("usr_users").where({ id }).first();
716
+ if (!user) throw new errors.NotFoundError("Usuario");
717
+ return user;
718
+ },
719
+ async create(input, userId) {
720
+ const existing = await db2("usr_users").where({ email: input.email }).first();
721
+ if (existing) {
722
+ throw new errors.ConflictError("El email ya est\xE1 en uso");
723
+ }
724
+ const role = await db2("rol_roles").where({ id: input.role_id }).first();
725
+ if (!role) {
726
+ throw new errors.NotFoundError("Rol");
727
+ }
728
+ const hashedPassword = await hashPassword(input.password);
729
+ const id = generateId2();
730
+ await db2("usr_users").insert({
731
+ id,
732
+ email: input.email,
733
+ password: hashedPassword,
734
+ name: input.name,
735
+ role_id: input.role_id,
736
+ created_by: userId ?? null
737
+ });
738
+ const user = await db2("usr_users").where({ id }).first();
739
+ return excludePassword(user);
740
+ },
741
+ async update(id, input, userId) {
742
+ const currentUser = await db2("usr_users").where({ id }).first();
743
+ if (!currentUser) throw new errors.NotFoundError("Usuario");
744
+ return db2.transaction(async (trx) => {
745
+ if (input.email && input.email !== currentUser.email) {
746
+ const existing = await trx("usr_users").where({ email: input.email }).whereNot({ id }).first();
747
+ if (existing) {
748
+ throw new errors.ConflictError("El email ya est\xE1 en uso");
749
+ }
750
+ }
751
+ if (input.role_id) {
752
+ const role = await trx("rol_roles").where({ id: input.role_id }).first();
753
+ if (!role) {
754
+ throw new errors.NotFoundError("Rol");
755
+ }
756
+ }
757
+ const data = {
758
+ ...input,
759
+ updated_at: /* @__PURE__ */ new Date(),
760
+ updated_by: userId ?? null
761
+ };
762
+ if (input.password) {
763
+ data["password"] = await hashPassword(input.password);
764
+ }
765
+ await trx("usr_users").where({ id }).update(data);
766
+ const user = await trx("usr_users").where({ id }).first();
767
+ return excludePassword(user);
768
+ });
769
+ },
770
+ async delete(id) {
771
+ const user = await db2("usr_users").where({ id }).first();
772
+ if (!user) throw new errors.NotFoundError("Usuario");
773
+ await db2("usr_users").where({ id }).delete();
774
+ }
775
+ };
776
+ }
777
+ var USER_WITH_ROLE_COLUMNS;
778
+ var init_users_service = __esm({
779
+ "src/modules/users/users.service.ts"() {
780
+ "use strict";
781
+ init_auth_utils();
782
+ USER_WITH_ROLE_COLUMNS = [
783
+ "usr_users.*",
784
+ "rol_roles.name as role_name",
785
+ "rol_roles.description as role_description",
786
+ "rol_roles.is_system as role_is_system",
787
+ "rol_roles.created_at as role_created_at",
788
+ "rol_roles.updated_at as role_updated_at"
789
+ ];
790
+ }
791
+ });
792
+
793
+ // src/modules/users/users.controller.ts
794
+ function createUsersController(ctx) {
795
+ const { errors, abilities } = ctx;
796
+ const { subject: subject2, ForbiddenError: CASLForbiddenError3 } = abilities;
797
+ const usersService = createUsersService(ctx);
798
+ return {
799
+ async findAll(req, res) {
800
+ const authReq = req;
801
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("read", "UsrUser");
802
+ const result = await usersService.findAll(req.validated?.query);
803
+ res.json({ success: true, data: result });
804
+ },
805
+ async findById(req, res) {
806
+ const authReq = req;
807
+ const { id } = req.validated?.params;
808
+ const user = await usersService.findById(id);
809
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("read", subject2("UsrUser", user));
810
+ res.json({ success: true, data: user });
811
+ },
812
+ async create(req, res) {
813
+ const authReq = req;
814
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("create", "UsrUser");
815
+ const user = await usersService.create(req.body, authReq.user.id);
816
+ res.status(201).json({ success: true, data: user });
817
+ },
818
+ async update(req, res) {
819
+ const authReq = req;
820
+ const { id } = req.validated?.params;
821
+ const input = req.body;
822
+ const user = await usersService.findById(id);
823
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("update", subject2("UsrUser", user));
824
+ if (input.role_id && !authReq.ability.can("manage", "UsrUser")) {
825
+ throw new errors.ForbiddenError("No tienes permiso para cambiar roles");
826
+ }
827
+ const updated = await usersService.update(id, input, authReq.user.id);
828
+ res.json({ success: true, data: updated });
829
+ },
830
+ async delete(req, res) {
831
+ const authReq = req;
832
+ const { id } = req.validated?.params;
833
+ const user = await usersService.findById(id);
834
+ CASLForbiddenError3.from(authReq.ability).throwUnlessCan("delete", subject2("UsrUser", user));
835
+ if (authReq.user.id === id) {
836
+ throw new errors.ForbiddenError("No puedes eliminarte a ti mismo");
837
+ }
838
+ await usersService.delete(id);
839
+ res.json({ success: true, message: "Usuario eliminado" });
840
+ }
841
+ };
842
+ }
843
+ var init_users_controller = __esm({
844
+ "src/modules/users/users.controller.ts"() {
845
+ "use strict";
846
+ init_users_service();
847
+ }
848
+ });
849
+
850
+ // src/modules/users/users.schemas.ts
851
+ import { z as z2 } from "zod";
852
+ var createUserSchema, updateUserSchema, userParamsSchema, userQuerySchema;
853
+ var init_users_schemas = __esm({
854
+ "src/modules/users/users.schemas.ts"() {
855
+ "use strict";
856
+ createUserSchema = z2.object({
857
+ email: z2.string().email("Email inv\xE1lido"),
858
+ password: z2.string().min(6, "Password m\xEDnimo 6 caracteres"),
859
+ name: z2.string().min(1, "Nombre requerido"),
860
+ role_id: z2.string().min(1, "Rol requerido"),
861
+ metadata: z2.record(z2.unknown()).nullable().optional()
862
+ });
863
+ updateUserSchema = z2.object({
864
+ email: z2.string().email("Email inv\xE1lido").optional(),
865
+ password: z2.string().min(6, "Password m\xEDnimo 6 caracteres").optional(),
866
+ name: z2.string().min(1, "Nombre requerido").optional(),
867
+ role_id: z2.string().min(1).optional(),
868
+ metadata: z2.record(z2.unknown()).nullable().optional()
869
+ });
870
+ userParamsSchema = z2.object({
871
+ id: z2.string().min(1)
872
+ });
873
+ userQuerySchema = z2.object({
874
+ page: z2.coerce.number().min(1).default(1),
875
+ limit: z2.coerce.number().min(1).max(100).default(10),
876
+ role_id: z2.string().optional()
877
+ });
878
+ }
879
+ });
880
+
881
+ // src/modules/users/users.routes.ts
882
+ function createUsersRoutes(ctx) {
883
+ const router = ctx.createRouter();
884
+ const { validate: validate2, auth } = ctx.middleware;
885
+ const controller = createUsersController(ctx);
886
+ router.use(auth);
887
+ router.get("/", validate2({ query: userQuerySchema }), controller.findAll);
888
+ router.get("/:id", validate2({ params: userParamsSchema }), controller.findById);
889
+ router.post("/", validate2({ body: createUserSchema }), controller.create);
890
+ router.put("/:id", validate2({ params: userParamsSchema, body: updateUserSchema }), controller.update);
891
+ router.delete("/:id", validate2({ params: userParamsSchema }), controller.delete);
892
+ return router;
893
+ }
894
+ var init_users_routes = __esm({
895
+ "src/modules/users/users.routes.ts"() {
896
+ "use strict";
897
+ init_users_controller();
898
+ init_users_schemas();
899
+ }
900
+ });
901
+
902
+ // src/modules/users/index.ts
903
+ var usersModule;
904
+ var init_users = __esm({
905
+ "src/modules/users/index.ts"() {
906
+ "use strict";
907
+ init_users_migrate();
908
+ init_users_seed();
909
+ init_users_routes();
910
+ usersModule = {
911
+ name: "users",
912
+ code: "USR",
913
+ label: "Users",
914
+ icon: "mdi:account-group-outline",
915
+ description: "User accounts, authentication profiles, and role assignments",
916
+ type: "core",
917
+ dependencies: ["roles"],
918
+ migrate: migrate2,
919
+ seed: seed2,
920
+ routes: createUsersRoutes,
921
+ routePrefix: "/users",
922
+ subjects: ["UsrUser"],
923
+ entities: [
924
+ {
925
+ name: "UsrUser",
926
+ label: "Users",
927
+ listFields: {
928
+ name: "Name",
929
+ email: "Email",
930
+ "role.name": "Role"
931
+ },
932
+ formFields: {
933
+ name: { label: "Name", type: "text", required: true },
934
+ email: { label: "Email", type: "email", required: true },
935
+ password: { label: "Password", type: "password", required: true, createOnly: true },
936
+ role_id: {
937
+ label: "Role",
938
+ type: "select",
939
+ required: true,
940
+ optionsEndpoint: "/roles",
941
+ optionValue: "id",
942
+ optionLabel: "name"
943
+ }
944
+ },
945
+ labelField: "name"
946
+ }
947
+ ]
948
+ };
949
+ }
950
+ });
951
+
952
+ // src/modules/auth/auth.migrate.ts
953
+ async function migrate3(ctx) {
954
+ const { db: db2, logger: logger2 } = ctx;
955
+ if (!await db2.schema.hasTable("auth_refresh_tokens")) {
956
+ await db2.schema.createTable("auth_refresh_tokens", (table) => {
957
+ table.string("id").primary();
958
+ table.string("token").unique().notNullable();
959
+ table.string("user_id").notNullable().references("id").inTable("usr_users").onDelete("CASCADE");
960
+ table.timestamp("expires_at").notNullable();
961
+ table.timestamp("created_at").defaultTo(db2.fn.now());
962
+ table.index("user_id");
963
+ table.index("expires_at");
964
+ });
965
+ logger2.info("Created table: auth_refresh_tokens");
966
+ }
967
+ }
968
+ var init_auth_migrate = __esm({
969
+ "src/modules/auth/auth.migrate.ts"() {
970
+ "use strict";
971
+ }
972
+ });
973
+
974
+ // src/config/env.ts
975
+ import { z as z3 } from "zod";
976
+ function resolveConfig(config) {
977
+ const jwtSecret = config?.jwt?.secret ?? env.JWT_SECRET;
978
+ if (!jwtSecret || jwtSecret.length < 32) {
979
+ throw new Error("JWT_SECRET must be at least 32 characters. Provide via config.jwt.secret or JWT_SECRET env var.");
980
+ }
981
+ resolvedConfig = {
982
+ nodeEnv: env.NODE_ENV,
983
+ port: config?.port ?? env.PORT,
984
+ host: config?.host ?? "0.0.0.0",
985
+ homePath: config?.homePath ?? env.HOME_PATH,
986
+ corsOrigin: (Array.isArray(config?.cors?.origin) ? config.cors.origin[0] : config?.cors?.origin) ?? env.CORS_ORIGIN,
987
+ databaseUrl: config?.database?.url ?? env.DATABASE_URL,
988
+ jwtSecret,
989
+ jwtAccessExpires: config?.jwt?.accessExpires ?? env.JWT_ACCESS_EXPIRES,
990
+ jwtRefreshExpires: config?.jwt?.refreshExpires ?? env.JWT_REFRESH_EXPIRES,
991
+ adminEmail: config?.admin?.email ?? env.ADMIN_EMAIL,
992
+ adminPassword: config?.admin?.password ?? env.ADMIN_PASSWORD,
993
+ smtp: {
994
+ host: config?.smtp?.host ?? env.SMTP_HOST,
995
+ port: config?.smtp?.port ?? env.SMTP_PORT,
996
+ secure: config?.smtp?.secure ?? env.SMTP_SECURE,
997
+ auth: config?.smtp?.auth ?? env.SMTP_USER ? { user: config?.smtp?.auth?.user ?? env.SMTP_USER, pass: config?.smtp?.auth?.pass ?? env.SMTP_PASS } : void 0,
998
+ from: config?.smtp?.from ?? env.SMTP_FROM
999
+ }
1000
+ };
1001
+ return resolvedConfig;
1002
+ }
1003
+ function getConfig() {
1004
+ if (!resolvedConfig) {
1005
+ return resolveConfig();
1006
+ }
1007
+ return resolvedConfig;
1008
+ }
1009
+ function resetConfig() {
1010
+ resolvedConfig = null;
1011
+ }
1012
+ var envSchema, env, resolvedConfig;
1013
+ var init_env = __esm({
1014
+ "src/config/env.ts"() {
1015
+ "use strict";
1016
+ envSchema = z3.object({
1017
+ NODE_ENV: z3.enum(["development", "production", "test"]).default("development"),
1018
+ PORT: z3.coerce.number().default(3e3),
1019
+ HOME_PATH: z3.string().default("/ui"),
1020
+ CORS_ORIGIN: z3.string().default("http://localhost:3001"),
1021
+ BACKEND_URL: z3.string().optional(),
1022
+ DATABASE_URL: z3.string().default("file:./dev.db"),
1023
+ JWT_SECRET: z3.string().min(32).optional(),
1024
+ JWT_ACCESS_EXPIRES: z3.string().default("15m"),
1025
+ JWT_REFRESH_EXPIRES: z3.string().default("7d"),
1026
+ ADMIN_EMAIL: z3.string().email().optional(),
1027
+ ADMIN_PASSWORD: z3.string().min(6).optional(),
1028
+ COOKIE_DOMAIN: z3.string().optional(),
1029
+ // SMTP
1030
+ SMTP_HOST: z3.string().default("kendra.server.arpa"),
1031
+ SMTP_PORT: z3.coerce.number().default(1025),
1032
+ SMTP_SECURE: z3.coerce.boolean().default(false),
1033
+ SMTP_USER: z3.string().optional(),
1034
+ SMTP_PASS: z3.string().optional(),
1035
+ SMTP_FROM: z3.string().default("noreply@nexus.local")
1036
+ });
1037
+ env = envSchema.parse(process.env);
1038
+ resolvedConfig = null;
1039
+ }
1040
+ });
1041
+
1042
+ // src/modules/auth/jwt.utils.ts
1043
+ import jwt from "jsonwebtoken";
1044
+ import crypto from "crypto";
1045
+ function parseExpiration(exp) {
1046
+ const match = exp.match(/^(\d+)([smhd])$/);
1047
+ if (!match) return 900;
1048
+ const value = parseInt(match[1], 10);
1049
+ const unit = match[2];
1050
+ switch (unit) {
1051
+ case "s":
1052
+ return value;
1053
+ case "m":
1054
+ return value * 60;
1055
+ case "h":
1056
+ return value * 60 * 60;
1057
+ case "d":
1058
+ return value * 60 * 60 * 24;
1059
+ default:
1060
+ return 900;
1061
+ }
1062
+ }
1063
+ function generateAccessToken(payload) {
1064
+ const config = getConfig();
1065
+ return jwt.sign(payload, config.jwtSecret, {
1066
+ expiresIn: config.jwtAccessExpires
1067
+ });
1068
+ }
1069
+ function generateRefreshToken() {
1070
+ return crypto.randomBytes(64).toString("hex");
1071
+ }
1072
+ function getRefreshTokenExpiration() {
1073
+ const seconds = parseExpiration(getConfig().jwtRefreshExpires);
1074
+ return new Date(Date.now() + seconds * 1e3);
1075
+ }
1076
+ function generateTokenPair(payload) {
1077
+ return {
1078
+ accessToken: generateAccessToken(payload),
1079
+ refreshToken: generateRefreshToken()
1080
+ };
1081
+ }
1082
+ var init_jwt_utils = __esm({
1083
+ "src/modules/auth/jwt.utils.ts"() {
1084
+ "use strict";
1085
+ init_env();
1086
+ }
1087
+ });
1088
+
1089
+ // src/modules/auth/auth.service.ts
1090
+ function createAuthService(ctx) {
1091
+ const { db: db2, errors, generateId: generateId2, abilities, events } = ctx;
1092
+ const { defineAbilityFor: defineAbilityFor2, packRules: packRules2 } = abilities;
1093
+ const rolesService = createRolesService(ctx);
1094
+ async function getUserWithRole(userId) {
1095
+ const user = await db2("usr_users").leftJoin("rol_roles", "usr_users.role_id", "rol_roles.id").where("usr_users.id", userId).select(
1096
+ "usr_users.*",
1097
+ "rol_roles.name as role_name",
1098
+ "rol_roles.description as role_description",
1099
+ "rol_roles.is_system as role_is_system"
1100
+ ).first();
1101
+ if (!user) return null;
1102
+ const { role_name, role_description, role_is_system, password: _, ...userWithoutPassword } = user;
1103
+ return {
1104
+ ...userWithoutPassword,
1105
+ role: {
1106
+ id: user.role_id,
1107
+ name: role_name,
1108
+ description: role_description,
1109
+ is_system: role_is_system
1110
+ }
1111
+ };
1112
+ }
1113
+ return {
1114
+ async login(input) {
1115
+ const user = await db2("usr_users").where({ email: input.email }).first();
1116
+ const hashToCheck = user?.password ?? DUMMY_HASH;
1117
+ const validPassword = await verifyPassword(input.password, hashToCheck);
1118
+ if (!user || !validPassword) {
1119
+ events.emitEvent("auth.failed", {
1120
+ email: input.email,
1121
+ reason: user ? "invalid_password" : "user_not_found"
1122
+ });
1123
+ throw new errors.UnauthorizedError("Credenciales inv\xE1lidas");
1124
+ }
1125
+ const tokens = generateTokenPair({
1126
+ userId: user.id,
1127
+ email: user.email,
1128
+ roleId: user.role_id
1129
+ });
1130
+ await db2("auth_refresh_tokens").insert({
1131
+ id: generateId2(),
1132
+ token: tokens.refreshToken,
1133
+ user_id: user.id,
1134
+ expires_at: getRefreshTokenExpiration()
1135
+ });
1136
+ const permissions = await rolesService.getPermissionsByRoleId(user.role_id);
1137
+ const ability = defineAbilityFor2(user, permissions);
1138
+ const userWithRole = await getUserWithRole(user.id);
1139
+ events.emitEvent("auth.login", { userId: user.id, email: user.email });
1140
+ return {
1141
+ user: userWithRole,
1142
+ accessToken: tokens.accessToken,
1143
+ refreshToken: tokens.refreshToken,
1144
+ abilities: packRules2(ability)
1145
+ };
1146
+ },
1147
+ async refresh(refreshToken) {
1148
+ const result = await db2.transaction(async (trx) => {
1149
+ let query = trx("auth_refresh_tokens").where({ token: refreshToken });
1150
+ if (ctx.dbType !== "sqlite") {
1151
+ query = query.forUpdate();
1152
+ }
1153
+ const storedToken = await query.first();
1154
+ if (!storedToken) {
1155
+ throw new errors.UnauthorizedError("Refresh token inv\xE1lido");
1156
+ }
1157
+ if (new Date(storedToken.expires_at) < /* @__PURE__ */ new Date()) {
1158
+ await trx("auth_refresh_tokens").where({ id: storedToken.id }).delete();
1159
+ throw new errors.UnauthorizedError("Refresh token expirado");
1160
+ }
1161
+ const user = await trx("usr_users").where({ id: storedToken.user_id }).first();
1162
+ if (!user) {
1163
+ throw new errors.UnauthorizedError("Usuario no encontrado");
1164
+ }
1165
+ const tokens = generateTokenPair({
1166
+ userId: user.id,
1167
+ email: user.email,
1168
+ roleId: user.role_id
1169
+ });
1170
+ await trx("auth_refresh_tokens").where({ id: storedToken.id }).delete();
1171
+ await trx("auth_refresh_tokens").insert({
1172
+ id: generateId2(),
1173
+ token: tokens.refreshToken,
1174
+ user_id: user.id,
1175
+ expires_at: getRefreshTokenExpiration()
1176
+ });
1177
+ return { user, tokens };
1178
+ });
1179
+ const permissions = await rolesService.getPermissionsByRoleId(result.user.role_id);
1180
+ const ability = defineAbilityFor2(result.user, permissions);
1181
+ events.emitEvent("auth.refresh", { userId: result.user.id });
1182
+ return {
1183
+ accessToken: result.tokens.accessToken,
1184
+ refreshToken: result.tokens.refreshToken,
1185
+ abilities: packRules2(ability)
1186
+ };
1187
+ },
1188
+ async logout(refreshToken, userId) {
1189
+ const deleted = await db2("auth_refresh_tokens").where({ token: refreshToken }).delete();
1190
+ if (deleted > 0 && userId) {
1191
+ events.emitEvent("auth.logout", { userId });
1192
+ }
1193
+ return deleted > 0;
1194
+ },
1195
+ async me(userId) {
1196
+ const userWithRole = await getUserWithRole(userId);
1197
+ if (!userWithRole) {
1198
+ throw new errors.NotFoundError("Usuario");
1199
+ }
1200
+ const permissions = await rolesService.getPermissionsByRoleId(userWithRole.role_id);
1201
+ const ability = defineAbilityFor2(userWithRole, permissions);
1202
+ return {
1203
+ user: userWithRole,
1204
+ abilities: packRules2(ability)
1205
+ };
1206
+ },
1207
+ async cleanupExpiredTokens() {
1208
+ const deleted = await db2("auth_refresh_tokens").where("expires_at", "<", /* @__PURE__ */ new Date()).delete();
1209
+ return deleted;
1210
+ }
1211
+ };
1212
+ }
1213
+ var init_auth_service = __esm({
1214
+ "src/modules/auth/auth.service.ts"() {
1215
+ "use strict";
1216
+ init_roles_service();
1217
+ init_jwt_utils();
1218
+ init_auth_utils();
1219
+ }
1220
+ });
1221
+
1222
+ // src/modules/auth/auth.controller.ts
1223
+ function createAuthController(ctx) {
1224
+ const { errors } = ctx;
1225
+ const authService = createAuthService(ctx);
1226
+ function getCookieOptions(req) {
1227
+ const isSecure = req.secure || req.get("x-forwarded-proto") === "https";
1228
+ return {
1229
+ httpOnly: true,
1230
+ secure: isSecure,
1231
+ sameSite: isSecure ? "strict" : "lax",
1232
+ path: "/api/v1",
1233
+ maxAge: 7 * 24 * 60 * 60 * 1e3
1234
+ // 7 days
1235
+ };
1236
+ }
1237
+ return {
1238
+ async login(req, res) {
1239
+ const result = await authService.login(req.body);
1240
+ res.cookie("refreshToken", result.refreshToken, getCookieOptions(req));
1241
+ res.json({
1242
+ success: true,
1243
+ data: {
1244
+ user: result.user,
1245
+ accessToken: result.accessToken,
1246
+ abilities: result.abilities
1247
+ }
1248
+ });
1249
+ },
1250
+ async refresh(req, res) {
1251
+ const refreshToken = req.cookies?.["refreshToken"];
1252
+ if (!refreshToken) {
1253
+ throw new errors.UnauthorizedError("Refresh token required");
1254
+ }
1255
+ const result = await authService.refresh(refreshToken);
1256
+ res.cookie("refreshToken", result.refreshToken, getCookieOptions(req));
1257
+ res.json({
1258
+ success: true,
1259
+ data: {
1260
+ accessToken: result.accessToken,
1261
+ abilities: result.abilities
1262
+ }
1263
+ });
1264
+ },
1265
+ async logout(req, res) {
1266
+ const refreshToken = req.cookies?.["refreshToken"];
1267
+ const authReq = req;
1268
+ if (refreshToken) {
1269
+ await authService.logout(refreshToken, authReq.user?.id);
1270
+ }
1271
+ res.clearCookie("refreshToken", { path: "/api/v1" });
1272
+ res.json({ success: true, message: "Session closed" });
1273
+ },
1274
+ async me(req, res) {
1275
+ const authReq = req;
1276
+ const result = await authService.me(authReq.user.id);
1277
+ res.json({ success: true, data: result });
1278
+ }
1279
+ };
1280
+ }
1281
+ var init_auth_controller = __esm({
1282
+ "src/modules/auth/auth.controller.ts"() {
1283
+ "use strict";
1284
+ init_auth_service();
1285
+ }
1286
+ });
1287
+
1288
+ // src/modules/auth/auth.schemas.ts
1289
+ import { z as z4 } from "zod";
1290
+ var loginSchema;
1291
+ var init_auth_schemas = __esm({
1292
+ "src/modules/auth/auth.schemas.ts"() {
1293
+ "use strict";
1294
+ loginSchema = z4.object({
1295
+ email: z4.string().email("Email inv\xE1lido"),
1296
+ password: z4.string().min(1, "Password requerido")
1297
+ });
1298
+ }
1299
+ });
1300
+
1301
+ // src/modules/auth/auth.routes.ts
1302
+ function createAuthRoutes(ctx) {
1303
+ const router = ctx.createRouter();
1304
+ const { validate: validate2, auth } = ctx.middleware;
1305
+ const controller = createAuthController(ctx);
1306
+ router.post("/login", validate2({ body: loginSchema }), controller.login);
1307
+ router.post("/refresh", controller.refresh);
1308
+ router.post("/logout", auth, controller.logout);
1309
+ router.get("/me", auth, controller.me);
1310
+ return router;
1311
+ }
1312
+ var init_auth_routes = __esm({
1313
+ "src/modules/auth/auth.routes.ts"() {
1314
+ "use strict";
1315
+ init_auth_controller();
1316
+ init_auth_schemas();
1317
+ }
1318
+ });
1319
+
1320
+ // src/modules/auth/auth.middleware.ts
1321
+ import jwt2 from "jsonwebtoken";
1322
+ function createAuthMiddleware(ctx) {
1323
+ const { config, errors, abilities } = ctx;
1324
+ const { defineAbilityFor: defineAbilityFor2 } = abilities;
1325
+ const rolesService = createRolesService(ctx);
1326
+ return async (req, _res, next) => {
1327
+ const authHeader = req.headers.authorization;
1328
+ if (!authHeader?.startsWith("Bearer ")) {
1329
+ throw new errors.UnauthorizedError("Token no proporcionado");
1330
+ }
1331
+ const token = authHeader.slice(7);
1332
+ try {
1333
+ const decoded = jwt2.verify(token, config.jwtSecret);
1334
+ const user = await ctx.db("usr_users").where({ id: decoded.userId }).first();
1335
+ if (!user) {
1336
+ throw new errors.UnauthorizedError("Usuario no encontrado");
1337
+ }
1338
+ const permissions = await rolesService.getPermissionsByRoleId(user.role_id);
1339
+ req.user = user;
1340
+ req.ability = defineAbilityFor2(user, permissions);
1341
+ next();
1342
+ } catch (error) {
1343
+ if (error instanceof jwt2.JsonWebTokenError) {
1344
+ throw new errors.UnauthorizedError("Token inv\xE1lido");
1345
+ }
1346
+ if (error instanceof jwt2.TokenExpiredError) {
1347
+ throw new errors.UnauthorizedError("Token expirado");
1348
+ }
1349
+ throw error;
1350
+ }
1351
+ };
1352
+ }
1353
+ function createOptionalAuthMiddleware(ctx) {
1354
+ const { config, abilities } = ctx;
1355
+ const { defineAbilityFor: defineAbilityFor2 } = abilities;
1356
+ const rolesService = createRolesService(ctx);
1357
+ return async (req, _res, next) => {
1358
+ const authHeader = req.headers.authorization;
1359
+ if (!authHeader?.startsWith("Bearer ")) {
1360
+ return next();
1361
+ }
1362
+ const token = authHeader.slice(7);
1363
+ try {
1364
+ const decoded = jwt2.verify(token, config.jwtSecret);
1365
+ const user = await ctx.db("usr_users").where({ id: decoded.userId }).first();
1366
+ if (user) {
1367
+ const permissions = await rolesService.getPermissionsByRoleId(user.role_id);
1368
+ req.user = user;
1369
+ req.ability = defineAbilityFor2(user, permissions);
1370
+ }
1371
+ } catch {
1372
+ }
1373
+ next();
1374
+ };
1375
+ }
1376
+ var init_auth_middleware = __esm({
1377
+ "src/modules/auth/auth.middleware.ts"() {
1378
+ "use strict";
1379
+ init_roles_service();
1380
+ }
1381
+ });
1382
+
1383
+ // src/modules/auth/index.ts
1384
+ var authModule;
1385
+ var init_auth = __esm({
1386
+ "src/modules/auth/index.ts"() {
1387
+ "use strict";
1388
+ init_auth_migrate();
1389
+ init_auth_routes();
1390
+ init_auth_middleware();
1391
+ authModule = {
1392
+ name: "auth",
1393
+ code: "AUTH",
1394
+ label: "Authentication",
1395
+ icon: "mdi:lock-outline",
1396
+ description: "JWT authentication with refresh tokens, session management, and password security",
1397
+ type: "core",
1398
+ dependencies: ["users"],
1399
+ migrate: migrate3,
1400
+ init: (ctx) => {
1401
+ ctx.registerMiddleware("auth", createAuthMiddleware(ctx));
1402
+ ctx.registerMiddleware("optionalAuth", createOptionalAuthMiddleware(ctx));
1403
+ },
1404
+ routes: createAuthRoutes,
1405
+ routePrefix: "/auth",
1406
+ subjects: []
1407
+ };
1408
+ }
1409
+ });
1410
+
1411
+ // src/modules/index.ts
1412
+ function validateModuleConventions(mod) {
1413
+ const errors = [];
1414
+ if (!/^[A-Z]+$/.test(mod.code)) {
1415
+ errors.push(`code debe ser UPPERCASE (recibido: '${mod.code}')`);
1416
+ }
1417
+ const codePrefix = mod.code.charAt(0) + mod.code.slice(1).toLowerCase();
1418
+ for (const subject2 of mod.subjects ?? []) {
1419
+ if (!subject2.startsWith(codePrefix)) {
1420
+ errors.push(`subject '${subject2}' debe empezar con '${codePrefix}' (patr\xF3n: {Code}{Entity})`);
1421
+ }
1422
+ }
1423
+ for (const entity of mod.entities ?? []) {
1424
+ if (!entity.name.startsWith(codePrefix)) {
1425
+ errors.push(`entity.name '${entity.name}' debe empezar con '${codePrefix}' (patr\xF3n: {Code}{Entity})`);
1426
+ }
1427
+ }
1428
+ if (errors.length > 0) {
1429
+ throw new Error(`M\xF3dulo '${mod.name}' viola convenciones de nombrado:
1430
+ - ${errors.join("\n - ")}`);
1431
+ }
1432
+ }
1433
+ function getOrderedModules() {
1434
+ const sorted = [];
1435
+ const visited = /* @__PURE__ */ new Set();
1436
+ const moduleMap = new Map(modules.map((m) => [m.name, m]));
1437
+ function visit(mod) {
1438
+ if (visited.has(mod.name)) return;
1439
+ visited.add(mod.name);
1440
+ for (const dep of mod.dependencies ?? []) {
1441
+ const depMod = moduleMap.get(dep);
1442
+ if (depMod) visit(depMod);
1443
+ }
1444
+ sorted.push(mod);
1445
+ }
1446
+ modules.forEach(visit);
1447
+ return sorted;
1448
+ }
1449
+ function registerModule(mod) {
1450
+ validateModuleConventions(mod);
1451
+ modules.push(mod);
1452
+ }
1453
+ function getModules() {
1454
+ return [...modules];
1455
+ }
1456
+ function getModule(name) {
1457
+ return modules.find((m) => m.name === name);
1458
+ }
1459
+ function getRegisteredSubjects() {
1460
+ const subjects = /* @__PURE__ */ new Set(["all"]);
1461
+ for (const mod of modules) {
1462
+ for (const subject2 of mod.subjects ?? []) {
1463
+ subjects.add(subject2);
1464
+ }
1465
+ for (const entity of mod.entities ?? []) {
1466
+ subjects.add(entity.name);
1467
+ }
1468
+ }
1469
+ return [...subjects];
1470
+ }
1471
+ function isValidSubject(subject2) {
1472
+ return getRegisteredSubjects().includes(subject2);
1473
+ }
1474
+ var modules;
1475
+ var init_modules = __esm({
1476
+ "src/modules/index.ts"() {
1477
+ "use strict";
1478
+ init_system();
1479
+ init_roles();
1480
+ init_users();
1481
+ init_auth();
1482
+ modules = [];
1483
+ [systemModule, rolesModule, usersModule, authModule].forEach((mod) => {
1484
+ validateModuleConventions(mod);
1485
+ modules.push(mod);
1486
+ });
1487
+ }
1488
+ });
1489
+
1490
+ // src/events/emitter.ts
1491
+ import pkg from "eventemitter2";
1492
+ var EventEmitter2, TypedEventEmitter, nexusEvents;
1493
+ var init_emitter = __esm({
1494
+ "src/events/emitter.ts"() {
1495
+ "use strict";
1496
+ ({ EventEmitter2 } = pkg);
1497
+ TypedEventEmitter = class extends EventEmitter2 {
1498
+ emitEvent(event, ...args) {
1499
+ return this.emit(event, ...args);
1500
+ }
1501
+ onEvent(event, listener) {
1502
+ this.on(event, listener);
1503
+ return this;
1504
+ }
1505
+ onceEvent(event, listener) {
1506
+ this.once(event, listener);
1507
+ return this;
1508
+ }
1509
+ offEvent(event, listener) {
1510
+ this.off(event, listener);
1511
+ return this;
1512
+ }
1513
+ };
1514
+ nexusEvents = new TypedEventEmitter({
1515
+ wildcard: true,
1516
+ delimiter: ".",
1517
+ maxListeners: 20,
1518
+ verboseMemoryLeak: true
1519
+ });
1520
+ }
1521
+ });
1522
+
1523
+ // src/db/query-interceptor.ts
1524
+ function setupQueryInterceptor(knexInstance2) {
1525
+ knexInstance2.on("query-response", (response, query) => {
1526
+ const sql = query.sql?.toLowerCase() ?? "";
1527
+ let action;
1528
+ let table;
1529
+ if (sql.startsWith("insert into")) {
1530
+ action = "created";
1531
+ table = extractTableFromInsert(sql);
1532
+ } else if (sql.startsWith("update")) {
1533
+ action = "updated";
1534
+ table = extractTableFromUpdate(sql);
1535
+ } else if (sql.startsWith("delete from")) {
1536
+ action = "deleted";
1537
+ table = extractTableFromDelete(sql);
1538
+ }
1539
+ if (action && table && !IGNORED_TABLES.has(table)) {
1540
+ nexusEvents.emit(`db.${table}.${action}`, {
1541
+ table,
1542
+ action,
1543
+ data: response,
1544
+ timestamp: /* @__PURE__ */ new Date()
1545
+ });
1546
+ }
1547
+ });
1548
+ return knexInstance2;
1549
+ }
1550
+ function extractTableFromInsert(sql) {
1551
+ const match = sql.match(/insert into\s+["'`]?(\w+)["'`]?/i);
1552
+ return match?.[1];
1553
+ }
1554
+ function extractTableFromUpdate(sql) {
1555
+ const match = sql.match(/update\s+["'`]?(\w+)["'`]?/i);
1556
+ return match?.[1];
1557
+ }
1558
+ function extractTableFromDelete(sql) {
1559
+ const match = sql.match(/delete from\s+["'`]?(\w+)["'`]?/i);
1560
+ return match?.[1];
1561
+ }
1562
+ var IGNORED_TABLES;
1563
+ var init_query_interceptor = __esm({
1564
+ "src/db/query-interceptor.ts"() {
1565
+ "use strict";
1566
+ init_emitter();
1567
+ IGNORED_TABLES = /* @__PURE__ */ new Set(["refresh_tokens", "knex_migrations", "knex_migrations_lock"]);
1568
+ }
1569
+ });
1570
+
1571
+ // src/config/database.ts
1572
+ import knex from "knex";
1573
+ function getDatabaseConfig() {
1574
+ const url = env.DATABASE_URL;
1575
+ if (url.startsWith("file:") || url.startsWith("sqlite:")) {
1576
+ const filename = url.replace(/^(file:|sqlite:)/, "");
1577
+ return {
1578
+ client: "better-sqlite3",
1579
+ connection: { filename },
1580
+ useNullAsDefault: true
1581
+ };
1582
+ }
1583
+ if (url.startsWith("postgresql://") || url.startsWith("postgres://")) {
1584
+ return {
1585
+ client: "pg",
1586
+ connection: url,
1587
+ pool: { min: 2, max: 10 }
1588
+ };
1589
+ }
1590
+ if (url.startsWith("mysql://")) {
1591
+ return {
1592
+ client: "mysql2",
1593
+ connection: url,
1594
+ pool: { min: 2, max: 10 }
1595
+ };
1596
+ }
1597
+ throw new Error(`Unsupported database URL: ${url}`);
1598
+ }
1599
+ function getDatabaseType() {
1600
+ const url = env.DATABASE_URL;
1601
+ if (url.startsWith("file:") || url.startsWith("sqlite:")) return "sqlite";
1602
+ if (url.startsWith("postgresql://") || url.startsWith("postgres://")) return "postgresql";
1603
+ if (url.startsWith("mysql://")) return "mysql";
1604
+ return "sqlite";
1605
+ }
1606
+ async function destroyDb() {
1607
+ if (globalForKnex.db) {
1608
+ await globalForKnex.db.destroy();
1609
+ globalForKnex.db = void 0;
1610
+ }
1611
+ }
1612
+ var globalForKnex, knexInstance, db;
1613
+ var init_database = __esm({
1614
+ "src/config/database.ts"() {
1615
+ "use strict";
1616
+ init_env();
1617
+ init_query_interceptor();
1618
+ globalForKnex = globalThis;
1619
+ knexInstance = globalForKnex.db ?? knex(getDatabaseConfig());
1620
+ db = globalForKnex.db ? knexInstance : setupQueryInterceptor(knexInstance);
1621
+ if (env.NODE_ENV !== "production") {
1622
+ globalForKnex.db = db;
1623
+ }
1624
+ }
1625
+ });
1626
+
1627
+ // src/shared/logger.ts
1628
+ import pino from "pino";
1629
+ var isDev, logger;
1630
+ var init_logger = __esm({
1631
+ "src/shared/logger.ts"() {
1632
+ "use strict";
1633
+ isDev = process.env["NODE_ENV"] !== "production";
1634
+ logger = pino({
1635
+ level: process.env["LOG_LEVEL"] || "info",
1636
+ transport: isDev ? { target: "pino-pretty", options: { colorize: true } } : void 0
1637
+ });
1638
+ }
1639
+ });
1640
+
1641
+ // src/shared/utils/id.ts
1642
+ import crypto2 from "crypto";
1643
+ function generateId() {
1644
+ const timestamp = Date.now().toString(36);
1645
+ const randomPart = crypto2.randomBytes(8).toString("base64url");
1646
+ return `${timestamp}${randomPart}`.slice(0, 25);
1647
+ }
1648
+ var init_id = __esm({
1649
+ "src/shared/utils/id.ts"() {
1650
+ "use strict";
1651
+ }
1652
+ });
1653
+
1654
+ // src/db/helpers.ts
1655
+ function addTimestamps(table, db2) {
1656
+ table.timestamp("created_at").defaultTo(db2.fn.now());
1657
+ table.timestamp("updated_at").defaultTo(db2.fn.now());
1658
+ }
1659
+ async function addAuditFieldsIfMissing(db2, tableName) {
1660
+ if (!await db2.schema.hasColumn(tableName, "created_by")) {
1661
+ await db2.schema.alterTable(tableName, (table) => {
1662
+ table.string("created_by").nullable();
1663
+ table.string("updated_by").nullable();
1664
+ });
1665
+ logger.info(`Added audit fields to: ${tableName}`);
1666
+ }
1667
+ }
1668
+ async function addColumnIfMissing(db2, tableName, columnName, columnBuilder) {
1669
+ if (!await db2.schema.hasColumn(tableName, columnName)) {
1670
+ await db2.schema.alterTable(tableName, columnBuilder);
1671
+ logger.info(`Added column: ${tableName}.${columnName}`);
1672
+ return true;
1673
+ }
1674
+ return false;
1675
+ }
1676
+ var init_helpers = __esm({
1677
+ "src/db/helpers.ts"() {
1678
+ "use strict";
1679
+ init_logger();
1680
+ }
1681
+ });
1682
+
1683
+ // src/shared/middleware/validate.middleware.ts
1684
+ function validate(schemas) {
1685
+ return (req, _res, next) => {
1686
+ req.validated = {};
1687
+ if (schemas.body) {
1688
+ req.body = schemas.body.parse(req.body);
1689
+ }
1690
+ if (schemas.query) {
1691
+ req.validated.query = schemas.query.parse(req.query);
1692
+ }
1693
+ if (schemas.params) {
1694
+ req.validated.params = schemas.params.parse(req.params);
1695
+ }
1696
+ next();
1697
+ };
1698
+ }
1699
+ var init_validate_middleware = __esm({
1700
+ "src/shared/middleware/validate.middleware.ts"() {
1701
+ "use strict";
1702
+ }
1703
+ });
1704
+
1705
+ // src/shared/errors/app-error.ts
1706
+ var AppError, NotFoundError, UnauthorizedError, ForbiddenError, ConflictError;
1707
+ var init_app_error = __esm({
1708
+ "src/shared/errors/app-error.ts"() {
1709
+ "use strict";
1710
+ AppError = class extends Error {
1711
+ statusCode;
1712
+ constructor(message, statusCode = 400) {
1713
+ super(message);
1714
+ this.statusCode = statusCode;
1715
+ this.name = "AppError";
1716
+ Error.captureStackTrace(this, this.constructor);
1717
+ }
1718
+ };
1719
+ NotFoundError = class extends AppError {
1720
+ constructor(resource = "Recurso") {
1721
+ super(`${resource} no encontrado`, 404);
1722
+ this.name = "NotFoundError";
1723
+ }
1724
+ };
1725
+ UnauthorizedError = class extends AppError {
1726
+ constructor(message = "No autorizado") {
1727
+ super(message, 401);
1728
+ this.name = "UnauthorizedError";
1729
+ }
1730
+ };
1731
+ ForbiddenError = class extends AppError {
1732
+ constructor(message = "Acceso denegado") {
1733
+ super(message, 403);
1734
+ this.name = "ForbiddenError";
1735
+ }
1736
+ };
1737
+ ConflictError = class extends AppError {
1738
+ constructor(message = "Conflicto") {
1739
+ super(message, 409);
1740
+ this.name = "ConflictError";
1741
+ }
1742
+ };
1743
+ }
1744
+ });
1745
+
1746
+ // src/shared/abilities/ability.factory.ts
1747
+ import { AbilityBuilder, createMongoAbility } from "@casl/ability";
1748
+ function interpolateConditions(conditions, user) {
1749
+ const result = {};
1750
+ for (const [key, value] of Object.entries(conditions)) {
1751
+ if (typeof value === "string" && value.startsWith("${user.")) {
1752
+ const prop = value.slice(7, -1);
1753
+ result[key] = user[prop];
1754
+ } else {
1755
+ result[key] = value;
1756
+ }
1757
+ }
1758
+ return result;
1759
+ }
1760
+ function defineAbilityFor(user, permissions) {
1761
+ const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
1762
+ for (const perm of permissions) {
1763
+ const action = perm.action;
1764
+ const subject2 = perm.subject;
1765
+ const conditions = perm.conditions ? interpolateConditions(perm.conditions, user) : void 0;
1766
+ const fields = perm.fields;
1767
+ if (perm.inverted) {
1768
+ if (fields?.length) {
1769
+ cannot(action, subject2, fields, conditions);
1770
+ } else {
1771
+ cannot(action, subject2, conditions);
1772
+ }
1773
+ } else {
1774
+ if (fields?.length) {
1775
+ can(action, subject2, fields, conditions);
1776
+ } else {
1777
+ can(action, subject2, conditions);
1778
+ }
1779
+ }
1780
+ }
1781
+ return build();
1782
+ }
1783
+ function packRules(ability) {
1784
+ return ability.rules;
1785
+ }
1786
+ function unpackRules(rules) {
1787
+ return createMongoAbility(rules);
1788
+ }
1789
+ var init_ability_factory = __esm({
1790
+ "src/shared/abilities/ability.factory.ts"() {
1791
+ "use strict";
1792
+ }
1793
+ });
1794
+
1795
+ // src/shared/mail/mail.service.ts
1796
+ import nodemailer from "nodemailer";
1797
+ import { readFileSync, existsSync } from "fs";
1798
+ import { dirname, join } from "path";
1799
+ import { fileURLToPath } from "url";
1800
+ var __dirname, TEMPLATE_PATH, LOGO_PATH, MailService;
1801
+ var init_mail_service = __esm({
1802
+ "src/shared/mail/mail.service.ts"() {
1803
+ "use strict";
1804
+ __dirname = dirname(fileURLToPath(import.meta.url));
1805
+ TEMPLATE_PATH = join(__dirname, "templates", "base.html");
1806
+ LOGO_PATH = join(__dirname, "../../../public/logo.png");
1807
+ MailService = class {
1808
+ transporter;
1809
+ defaultFrom;
1810
+ defaultLogoUrl;
1811
+ logger;
1812
+ template;
1813
+ constructor(config, logger2) {
1814
+ this.defaultFrom = config.from;
1815
+ this.logger = logger2.child({ service: "mail" });
1816
+ this.template = readFileSync(TEMPLATE_PATH, "utf-8");
1817
+ if (existsSync(LOGO_PATH)) {
1818
+ const logoBase64 = readFileSync(LOGO_PATH).toString("base64");
1819
+ this.defaultLogoUrl = `data:image/png;base64,${logoBase64}`;
1820
+ } else {
1821
+ this.defaultLogoUrl = "";
1822
+ }
1823
+ this.transporter = nodemailer.createTransport({
1824
+ host: config.host,
1825
+ port: config.port,
1826
+ secure: config.secure,
1827
+ auth: config.auth,
1828
+ // MailHog y servidores dev no necesitan TLS
1829
+ ...config.auth ? {} : { ignoreTLS: true }
1830
+ });
1831
+ }
1832
+ async send(options) {
1833
+ const from = options.from ?? this.defaultFrom;
1834
+ const to = Array.isArray(options.to) ? options.to.join(", ") : options.to;
1835
+ const html = options.title || options.message ? this.renderTemplate(options) : options.html;
1836
+ this.logger.info({ to, subject: options.subject }, "Sending email");
1837
+ try {
1838
+ const result = await this.transporter.sendMail({
1839
+ from,
1840
+ to,
1841
+ subject: options.subject,
1842
+ text: options.text,
1843
+ html,
1844
+ replyTo: options.replyTo,
1845
+ attachments: options.attachments
1846
+ });
1847
+ this.logger.info(
1848
+ { messageId: result.messageId, accepted: result.accepted },
1849
+ "Email sent"
1850
+ );
1851
+ return {
1852
+ messageId: result.messageId,
1853
+ accepted: result.accepted,
1854
+ rejected: result.rejected
1855
+ };
1856
+ } catch (error) {
1857
+ this.logger.warn({ error, to, subject: options.subject }, "Failed to send email (continuing)");
1858
+ return null;
1859
+ }
1860
+ }
1861
+ renderTemplate(options) {
1862
+ let html = this.template;
1863
+ html = html.replace(/\{\{subject\}\}/g, options.subject);
1864
+ html = html.replace(/\{\{logoUrl\}\}/g, options.logoUrl ?? this.defaultLogoUrl);
1865
+ html = html.replace(/\{\{year\}\}/g, (/* @__PURE__ */ new Date()).getFullYear().toString());
1866
+ html = this.processConditionalBlock(html, "title", options.title);
1867
+ html = this.processConditionalBlock(html, "message", options.message);
1868
+ if (options.actions?.length) {
1869
+ const actionsHtml = options.actions.map(
1870
+ (a) => `<a href="${a.url}" style="display:inline-block; background-color:#18181b; color:#ffffff; padding:12px 24px; text-decoration:none; border-radius:6px; margin:4px; font-weight:500;">${a.label}</a>`
1871
+ ).join("");
1872
+ html = html.replace(
1873
+ /\{\{#if actions\}\}([\s\S]*?)\{\{\/if\}\}/g,
1874
+ (_, content) => content.replace(/\{\{actions\}\}/g, actionsHtml)
1875
+ );
1876
+ } else {
1877
+ html = html.replace(/\{\{#if actions\}\}[\s\S]*?\{\{\/if\}\}/g, "");
1878
+ }
1879
+ return html;
1880
+ }
1881
+ processConditionalBlock(html, name, value) {
1882
+ const pattern = new RegExp(`\\{\\{#if ${name}\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}`, "g");
1883
+ if (value) {
1884
+ return html.replace(
1885
+ pattern,
1886
+ (_, content) => content.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value)
1887
+ );
1888
+ }
1889
+ return html.replace(pattern, "");
1890
+ }
1891
+ async verify() {
1892
+ try {
1893
+ await this.transporter.verify();
1894
+ this.logger.info("SMTP connection verified");
1895
+ return true;
1896
+ } catch (error) {
1897
+ this.logger.warn({ error }, "SMTP connection failed (emails may not work)");
1898
+ return false;
1899
+ }
1900
+ }
1901
+ };
1902
+ }
1903
+ });
1904
+
1905
+ // src/shared/mail/index.ts
1906
+ var init_mail = __esm({
1907
+ "src/shared/mail/index.ts"() {
1908
+ "use strict";
1909
+ init_mail_service();
1910
+ }
1911
+ });
1912
+
1913
+ // src/modules/context.ts
1914
+ import { Router } from "express";
1915
+ import { ForbiddenError as CASLForbiddenError, subject } from "@casl/ability";
1916
+ function createModuleContext() {
1917
+ const middleware = {
1918
+ validate
1919
+ };
1920
+ return {
1921
+ db,
1922
+ logger,
1923
+ generateId,
1924
+ dbType: getDatabaseType(),
1925
+ helpers: {
1926
+ addTimestamps,
1927
+ addAuditFieldsIfMissing,
1928
+ addColumnIfMissing
1929
+ },
1930
+ createRouter: () => Router(),
1931
+ middleware,
1932
+ registerMiddleware: (name, handler) => {
1933
+ middleware[name] = handler;
1934
+ },
1935
+ config: getConfig(),
1936
+ errors: {
1937
+ AppError,
1938
+ NotFoundError,
1939
+ UnauthorizedError,
1940
+ ForbiddenError,
1941
+ ConflictError
1942
+ },
1943
+ abilities: {
1944
+ defineAbilityFor,
1945
+ packRules,
1946
+ subject,
1947
+ ForbiddenError: CASLForbiddenError
1948
+ },
1949
+ events: nexusEvents,
1950
+ mail: new MailService(getConfig().smtp, logger)
1951
+ };
1952
+ }
1953
+ var init_context = __esm({
1954
+ "src/modules/context.ts"() {
1955
+ "use strict";
1956
+ init_database();
1957
+ init_env();
1958
+ init_logger();
1959
+ init_id();
1960
+ init_helpers();
1961
+ init_validate_middleware();
1962
+ init_app_error();
1963
+ init_ability_factory();
1964
+ init_emitter();
1965
+ init_mail();
1966
+ }
1967
+ });
1968
+
1969
+ // src/shared/middleware/error.middleware.ts
1970
+ import { ZodError } from "zod";
1971
+ import { ForbiddenError as CASLForbiddenError2 } from "@casl/ability";
1972
+ function errorMiddleware(err, req, res, _next) {
1973
+ if (env.NODE_ENV !== "production") {
1974
+ logger.error({ err, url: req.url, method: req.method }, "Error en request");
1975
+ }
1976
+ if (err instanceof ZodError) {
1977
+ return res.status(400).json({
1978
+ success: false,
1979
+ error: "Error de validaci\xF3n",
1980
+ details: err.errors.map((e) => ({
1981
+ path: e.path.join("."),
1982
+ message: e.message
1983
+ }))
1984
+ });
1985
+ }
1986
+ if (err instanceof CASLForbiddenError2) {
1987
+ return res.status(403).json({
1988
+ success: false,
1989
+ error: "No tienes permiso para esta acci\xF3n"
1990
+ });
1991
+ }
1992
+ if (err instanceof AppError) {
1993
+ return res.status(err.statusCode).json({
1994
+ success: false,
1995
+ error: err.message
1996
+ });
1997
+ }
1998
+ logger.error({ err, url: req.url, method: req.method, stack: err.stack }, "Unhandled error");
1999
+ res.status(500).json({
2000
+ success: false,
2001
+ error: env.NODE_ENV === "production" ? "Error interno del servidor" : err.message
2002
+ });
2003
+ }
2004
+ var init_error_middleware = __esm({
2005
+ "src/shared/middleware/error.middleware.ts"() {
2006
+ "use strict";
2007
+ init_app_error();
2008
+ init_env();
2009
+ init_logger();
2010
+ }
2011
+ });
2012
+
2013
+ // src/app.ts
2014
+ import express from "express";
2015
+ import cors from "cors";
2016
+ import helmet from "helmet";
2017
+ import compression from "compression";
2018
+ import cookieParser from "cookie-parser";
2019
+ import path from "path";
2020
+ import { fileURLToPath as fileURLToPath2 } from "url";
2021
+ import { randomUUID } from "crypto";
2022
+ function createApp() {
2023
+ const app = express();
2024
+ app.use(helmet({
2025
+ contentSecurityPolicy: env.NODE_ENV === "production" ? void 0 : false
2026
+ }));
2027
+ app.use(cors({
2028
+ origin: env.CORS_ORIGIN,
2029
+ credentials: true,
2030
+ allowedHeaders: ["Content-Type", "Authorization", "X-Correlation-ID", "X-Request-ID"],
2031
+ exposedHeaders: ["X-Correlation-ID", "X-Request-ID"]
2032
+ }));
2033
+ app.use((req, res, next) => {
2034
+ const requestId = randomUUID();
2035
+ const correlationId = req.get("X-Correlation-ID") || req.get("X-Request-ID") || requestId;
2036
+ res.setHeader("X-Request-ID", requestId);
2037
+ res.setHeader("X-Correlation-ID", correlationId);
2038
+ req.requestId = requestId;
2039
+ req.correlationId = correlationId;
2040
+ next();
2041
+ });
2042
+ const logLevel = process.env["LOG_LEVEL"];
2043
+ if (logLevel === "debug" || logLevel === "trace") {
2044
+ app.use((req, res, next) => {
2045
+ const start2 = Date.now();
2046
+ res.on("finish", () => {
2047
+ const ms = Date.now() - start2;
2048
+ const msg = `[${req.correlationId.slice(0, 8)}] ${req.method} ${req.originalUrl} ${res.statusCode} ${ms}ms`;
2049
+ if (req.originalUrl.startsWith("/api/")) {
2050
+ logger.debug(msg);
2051
+ } else {
2052
+ logger.trace(msg);
2053
+ }
2054
+ });
2055
+ next();
2056
+ });
2057
+ }
2058
+ app.use(compression());
2059
+ app.use(express.json());
2060
+ app.use(cookieParser());
2061
+ const ctx = createModuleContext();
2062
+ const modules2 = getModules();
2063
+ for (const mod of modules2) {
2064
+ if (mod.init) {
2065
+ mod.init(ctx);
2066
+ }
2067
+ }
2068
+ for (const mod of modules2) {
2069
+ if (mod.routes) {
2070
+ const prefix = mod.routePrefix ?? `/${mod.name}`;
2071
+ app.use(`/api/v1${prefix}`, mod.routes(ctx));
2072
+ }
2073
+ }
2074
+ app.get("/health", (_req, res) => {
2075
+ res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
2076
+ });
2077
+ app.get("/", (_req, res) => {
2078
+ res.redirect(getConfig().homePath);
2079
+ });
2080
+ const publicPath = path.join(__dirname2, "../public");
2081
+ app.use("/public", express.static(publicPath));
2082
+ const uiPath = path.join(__dirname2, "../ui/dist");
2083
+ app.use("/ui", express.static(uiPath));
2084
+ app.get("/ui/{*splat}", (_req, res) => {
2085
+ res.sendFile(path.join(uiPath, "index.html"));
2086
+ });
2087
+ app.use(errorMiddleware);
2088
+ return app;
2089
+ }
2090
+ var __dirname2;
2091
+ var init_app = __esm({
2092
+ "src/app.ts"() {
2093
+ "use strict";
2094
+ init_modules();
2095
+ init_context();
2096
+ init_error_middleware();
2097
+ init_env();
2098
+ init_logger();
2099
+ __dirname2 = path.dirname(fileURLToPath2(import.meta.url));
2100
+ }
2101
+ });
2102
+
2103
+ // src/shared/utils/net.ts
2104
+ import net from "net";
2105
+ async function checkPortAvailable(port, host = "0.0.0.0") {
2106
+ return new Promise((resolve, reject) => {
2107
+ const server2 = net.createServer();
2108
+ server2.once("error", (err) => {
2109
+ if (err.code === "EADDRINUSE") {
2110
+ logger.error({ port }, `Port ${port} is already in use`);
2111
+ reject(new Error(`Port ${port} is already in use`));
2112
+ } else {
2113
+ reject(err);
2114
+ }
2115
+ });
2116
+ server2.once("listening", () => {
2117
+ server2.close(() => resolve());
2118
+ });
2119
+ server2.listen(port, host);
2120
+ });
2121
+ }
2122
+ var init_net = __esm({
2123
+ "src/shared/utils/net.ts"() {
2124
+ "use strict";
2125
+ init_logger();
2126
+ }
2127
+ });
2128
+
2129
+ // src/server.ts
2130
+ var server_exports = {};
2131
+ __export(server_exports, {
2132
+ isRunning: () => isRunning,
2133
+ restart: () => restart,
2134
+ start: () => start,
2135
+ stop: () => stop
2136
+ });
2137
+ async function runMigrationsAndSeeds() {
2138
+ const ctx = createModuleContext();
2139
+ const modules2 = getOrderedModules();
2140
+ logger.info("Running migrations...");
2141
+ for (const mod of modules2) {
2142
+ if (mod.migrate) {
2143
+ await mod.migrate(ctx);
2144
+ }
2145
+ }
2146
+ logger.info("Running seeds...");
2147
+ for (const mod of modules2) {
2148
+ if (mod.seed) {
2149
+ await mod.seed(ctx);
2150
+ }
2151
+ }
2152
+ logger.info("Database ready");
2153
+ }
2154
+ async function start(config) {
2155
+ if (server) {
2156
+ throw new Error("Server already running. Call stop() first.");
2157
+ }
2158
+ const resolved = resolveConfig(config);
2159
+ await checkPortAvailable(resolved.port, resolved.host);
2160
+ nexusEvents.emitEvent("server.starting", { port: resolved.port, host: resolved.host });
2161
+ await runMigrationsAndSeeds();
2162
+ const app = createApp();
2163
+ return new Promise((resolve) => {
2164
+ server = app.listen(resolved.port, resolved.host, () => {
2165
+ const baseUrl = env.BACKEND_URL || `http://localhost:${resolved.port}`;
2166
+ logger.info({ port: resolved.port, mode: resolved.nodeEnv }, "Server started");
2167
+ logger.info(`API: ${baseUrl}/api/v1`);
2168
+ logger.info(`UI: ${baseUrl}/ui`);
2169
+ nexusEvents.emitEvent("server.started", { port: resolved.port, host: resolved.host });
2170
+ resolve(server);
2171
+ });
2172
+ });
2173
+ }
2174
+ async function stop() {
2175
+ if (!server) return;
2176
+ nexusEvents.emitEvent("server.stopping");
2177
+ await new Promise((resolve, reject) => {
2178
+ server.close((err) => {
2179
+ if (err) reject(err);
2180
+ else resolve();
2181
+ });
2182
+ });
2183
+ await destroyDb();
2184
+ nexusEvents.emitEvent("db.disconnected");
2185
+ resetConfig();
2186
+ server = null;
2187
+ nexusEvents.emitEvent("server.stopped");
2188
+ logger.info("Server stopped");
2189
+ }
2190
+ async function restart(config) {
2191
+ nexusEvents.emitEvent("server.restarting");
2192
+ await stop();
2193
+ return start(config);
2194
+ }
2195
+ function isRunning() {
2196
+ return server !== null;
2197
+ }
2198
+ function setupGracefulShutdown() {
2199
+ let shuttingDown = false;
2200
+ const shutdown = async (signal) => {
2201
+ if (shuttingDown) return;
2202
+ shuttingDown = true;
2203
+ logger.info({ signal }, "Graceful shutdown initiated");
2204
+ try {
2205
+ await stop();
2206
+ process.exit(0);
2207
+ } catch (err) {
2208
+ logger.error({ err }, "Error during shutdown");
2209
+ process.exit(1);
2210
+ }
2211
+ };
2212
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
2213
+ process.on("SIGINT", () => shutdown("SIGINT"));
2214
+ }
2215
+ var server;
2216
+ var init_server = __esm({
2217
+ "src/server.ts"() {
2218
+ "use strict";
2219
+ init_app();
2220
+ init_database();
2221
+ init_env();
2222
+ init_emitter();
2223
+ init_logger();
2224
+ init_net();
2225
+ init_modules();
2226
+ init_context();
2227
+ server = null;
2228
+ setupGracefulShutdown();
2229
+ }
2230
+ });
2231
+
2232
+ // src/index.ts
2233
+ init_server();
2234
+ init_app();
2235
+ init_database();
2236
+ init_env();
2237
+ init_modules();
2238
+ init_modules();
2239
+ init_ability_factory();
2240
+ init_emitter();
2241
+ import "dotenv/config";
2242
+ if (import.meta.url === `file://${process.argv[1]}`) {
2243
+ const { start: start2 } = await Promise.resolve().then(() => (init_server(), server_exports));
2244
+ start2();
2245
+ }
2246
+ export {
2247
+ createApp,
2248
+ db,
2249
+ defineAbilityFor,
2250
+ destroyDb,
2251
+ getConfig,
2252
+ getDatabaseType,
2253
+ getModule,
2254
+ getModules,
2255
+ getOrderedModules,
2256
+ getRegisteredSubjects,
2257
+ isRunning,
2258
+ isValidSubject,
2259
+ nexusEvents,
2260
+ packRules,
2261
+ registerModule,
2262
+ restart,
2263
+ start,
2264
+ stop,
2265
+ unpackRules
2266
+ };
2267
+ //# sourceMappingURL=index.js.map