@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/README.md +80 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +30 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +561 -0
- package/dist/index.js +2267 -0
- package/dist/index.js.map +1 -0
- package/package.json +97 -0
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
|