aloux-iam 0.0.139 → 0.0.141

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.
@@ -1,22 +1,79 @@
1
- const self = module.exports
1
+ const fs = require("fs");
2
+ const self = module.exports;
2
3
 
3
4
  self.responseError = async (res, error) => {
4
- let obj = error
5
- if (!error.code) {
6
- obj = {
7
- code: 400,
8
- title: 'Error',
9
- detail: error.message,
10
- suggestion: 'Revisar el detalle'
11
- }
12
- }
13
- res.status(obj.code).send(obj)
14
- }
5
+ let obj = error;
6
+ if (!error.code) {
7
+ obj = {
8
+ code: 400,
9
+ title: "Error",
10
+ detail: error.message,
11
+ suggestion: "Revisar el detalle",
12
+ };
13
+ }
14
+ res.status(obj.code).send(obj);
15
+ };
15
16
 
16
17
  self.generatePaginationResponse = async (count, page, itemsPerPage, items) => {
17
- const totalPages = Math.ceil(count / itemsPerPage)
18
- const currentPage = Math.max(1, Math.min(Number(page), totalPages))
19
- const finalCurrentPage = totalPages === 0 ? 1 : currentPage
20
- const remainingPages = Math.max(0, totalPages - finalCurrentPage)
21
- return { currentPage: finalCurrentPage, totalPages, perPage: Number(itemsPerPage), count, remainingPages, items }
22
- }
18
+ const totalPages = Math.ceil(count / itemsPerPage);
19
+ const currentPage = Math.max(1, Math.min(Number(page), totalPages));
20
+ const finalCurrentPage = totalPages === 0 ? 1 : currentPage;
21
+ const remainingPages = Math.max(0, totalPages - finalCurrentPage);
22
+ return { currentPage: finalCurrentPage, totalPages, perPage: Number(itemsPerPage), count, remainingPages, items };
23
+ };
24
+
25
+ self.brand = {
26
+ _cfg: null,
27
+
28
+ init(config) {
29
+ this._cfg = config;
30
+ },
31
+
32
+ _get(key) {
33
+ if (this._cfg) return this._cfg[key];
34
+ const APP = process.env.APP;
35
+ if (APP) return process.env[`${key}_${APP}`] || process.env[key];
36
+ return process.env[key];
37
+ },
38
+
39
+ name() {
40
+ return this._get("PROJECT_NAME");
41
+ },
42
+
43
+ template(key) {
44
+ const path = this._get(key);
45
+ if (!path) {
46
+ throw {
47
+ code: 500,
48
+ title: "Error de configuración",
49
+ detail: `Template no encontrado: ${key}`,
50
+ suggestion: "Verifica que la variable de entorno esté definida",
51
+ error: new Error(),
52
+ };
53
+ }
54
+ try {
55
+ let file = fs.readFileSync(path, "utf8");
56
+ file = file.replace(/\+\+\+brandName\+\+\+/g, this.name() || "");
57
+ file = file.replace(/\+\+\+brandColor\+\+\+/g, this._get("BRAND_COLOR") || "");
58
+ file = file.replace(/\+\+\+brandLogo\+\+\+/g, this._get("BRAND_LOGO") || "");
59
+ return file;
60
+ } catch (e) {
61
+ throw {
62
+ code: 500,
63
+ title: "Error al leer template",
64
+ detail: `No se pudo leer el archivo: ${path}`,
65
+ suggestion: "Verifica que el path del template sea correcto",
66
+ error: e,
67
+ };
68
+ }
69
+ },
70
+ };
71
+
72
+ // Gkey
73
+ self.resolveGkey = (business) => {
74
+ const businessGkey = business.gkey || null;
75
+ const hasOwnKey = businessGkey?.status === true;
76
+
77
+ if (hasOwnKey) return { gkey: businessGkey, source: "business" };
78
+ return { gkey: null, source: null };
79
+ };
@@ -1,7 +1,9 @@
1
1
  const Business = require("../models/Business");
2
+ const Company = require("../models/Company");
2
3
  const utils = require("../config/utils");
3
4
  const AlouxAWS = require("./operationsAWS");
4
5
  const errorController = require("../config/utils");
6
+ const { resolveGkey } = require("../config/utils");
5
7
 
6
8
  const self = module.exports;
7
9
 
@@ -11,16 +13,26 @@ self.create = async (req, res) => {
11
13
  req.body.lastUpdate = req.body.createdAt;
12
14
  req.body.status = "Activo";
13
15
 
16
+ let inheritedGkey = null;
17
+ if (req.body.data?.useCompanyKey && req.body._company) {
18
+ const company = await Company.findOne({ _id: req.body._company }).lean();
19
+ const companyGkey = company?.data?.gkey;
20
+ if (companyGkey?.status) inheritedGkey = companyGkey;
21
+ }
22
+
14
23
  if (req.body.environment && req.body.environment.length > 1) {
15
24
  for (let i in req.body.environment) {
16
25
  const business = new Business(req.body);
17
26
  business.environment = req.body.environment[i];
27
+ if (inheritedGkey) business.gkey = inheritedGkey;
18
28
  await business.save();
19
29
  }
20
30
  } else {
21
31
  const business = new Business(req.body);
32
+ if (inheritedGkey) business.gkey = inheritedGkey;
22
33
  await business.save();
23
34
  }
35
+
24
36
  await res.status(201).send({});
25
37
  } catch (error) {
26
38
  await errorController.responseError(res, error);
@@ -250,3 +262,56 @@ self.identity = async (req, res) => {
250
262
  await errorController.responseError(res, error);
251
263
  }
252
264
  };
265
+
266
+ self.setUseCompanyKey = async (req, res) => {
267
+ try {
268
+ const business = await Business.findOne({ _id: req.params.BUSINESS_ID }).lean();
269
+ if (!business) throw { code: 404, title: "No encontrado", detail: "No existe el negocio" };
270
+
271
+ await Business.updateOne(
272
+ { _id: req.params.BUSINESS_ID },
273
+ {
274
+ $unset: { gkey: "" },
275
+ $set: {
276
+ "data.useCompanyKey": false,
277
+ lastUpdate: new Date().getTime(),
278
+ },
279
+ }
280
+ );
281
+
282
+ res.status(202).send({ ok: true });
283
+ } catch (error) {
284
+ await errorController.responseError(res, error);
285
+ }
286
+ };
287
+
288
+ self.inheritKey = async (req, res) => {
289
+ try {
290
+ const business = await Business.findOne({ _id: req.params.BUSINESS_ID })
291
+ .populate("_company")
292
+ .lean();
293
+ if (!business) throw { code: 404, title: "No encontrado", detail: "No existe el negocio" };
294
+
295
+ const companyGkey = business._company?.data?.gkey;
296
+ if (!companyGkey?.status) throw {
297
+ code: 400,
298
+ title: "Sin llave",
299
+ detail: "La organización no tiene una llave de Google Cloud configurada",
300
+ };
301
+
302
+ await Business.updateOne(
303
+ { _id: req.params.BUSINESS_ID },
304
+ {
305
+ $set: {
306
+ gkey: companyGkey,
307
+ "data.useCompanyKey": true,
308
+ lastUpdate: new Date().getTime(),
309
+ },
310
+ }
311
+ );
312
+
313
+ res.status(202).send({ ok: true });
314
+ } catch (error) {
315
+ await errorController.responseError(res, error);
316
+ }
317
+ };
@@ -153,3 +153,38 @@ self.identity = async (req, res) => {
153
153
  await errorController.responseError(res, error);
154
154
  }
155
155
  };
156
+
157
+ self.updateGkey = async (req, res) => {
158
+ try {
159
+ const company = await Company.findOne({ _id: req.params.COMPANY_ID });
160
+ if (!company) throw { code: 404, title: "No encontrado", detail: "No existe la organización" };
161
+
162
+ company.data = { ...(company.data || {}), gkey: req.body.gkey };
163
+ company.lastUpdate = new Date().getTime();
164
+ company.markModified("data");
165
+ await company.save();
166
+
167
+ res.status(202).send({ ok: true });
168
+ } catch (error) {
169
+ await errorController.responseError(res, error);
170
+ }
171
+ };
172
+
173
+ self.deleteGkey = async (req, res) => {
174
+ try {
175
+ const company = await Company.findOne({ _id: req.params.COMPANY_ID });
176
+ if (!company) throw { code: 404, title: "No encontrado", detail: "No existe la organización" };
177
+
178
+ const newData = { ...(company.data || {}) };
179
+ delete newData.gkey;
180
+
181
+ company.data = newData;
182
+ company.lastUpdate = new Date().getTime();
183
+ company.markModified("data");
184
+ await company.save();
185
+
186
+ res.status(200).send({ ok: true });
187
+ } catch (error) {
188
+ await errorController.responseError(res, error);
189
+ }
190
+ };
@@ -0,0 +1,13 @@
1
+ const passwordService = require("../services/generatePassword");
2
+
3
+ const self = module.exports;
4
+
5
+ self.generate = (req, res) => {
6
+ try {
7
+ const length = Number(req.query.length) || 12;
8
+ const password = passwordService.generatePassword(length);
9
+ return res.json({ password });
10
+ } catch (error) {
11
+ return res.status(500).json({ message: "Error al generar la contraseña", error });
12
+ }
13
+ };
@@ -1,5 +1,6 @@
1
1
  const Log = require("../models/Log");
2
2
  const Label = require("../models/Label");
3
+ const mongoose = require("mongoose");
3
4
  const self = module.exports;
4
5
 
5
6
  self.create = async (req, res) => {
@@ -33,6 +34,7 @@ self.update = async (req, resp) => {
33
34
  resp.status(400).send({ error: error.message });
34
35
  }
35
36
  };
37
+
36
38
  self.status = async (req, resp) => {
37
39
  try {
38
40
  const _id = req.params.LOG_ID;
@@ -46,22 +48,79 @@ self.status = async (req, resp) => {
46
48
  resp.status(400).send({ error: error.message });
47
49
  }
48
50
  };
51
+
49
52
  self.retrieve = async (req, res) => {
50
53
  try {
51
54
  const companyId =
52
55
  req.header("Company") !== "undefined" ? req.header("Company") : null;
53
- let query = { _company: companyId };
54
56
 
55
- if (req.body.users.length) {
56
- query = {
57
- _user: { $in: req.body.users },
57
+ const matchStage = { _company: companyId };
58
+
59
+ if (req.body.users?.length) {
60
+ matchStage._user = {
61
+ $in: req.body.users.map((id) => new mongoose.Types.ObjectId(id)),
58
62
  };
59
63
  }
60
64
 
61
- const consulta = await Log.find(query)
62
- .populate([{ path: "_user", select: { name: 1, lastName: 1 } }])
63
- .sort({ createdAt: 1 })
64
- .lean();
65
+ if (req.body.dateStart || req.body.dateEnd) {
66
+ matchStage.createdAt = {};
67
+ if (req.body.dateStart)
68
+ matchStage.createdAt.$gte = Number(req.body.dateStart);
69
+ if (req.body.dateEnd)
70
+ matchStage.createdAt.$lte = Number(req.body.dateEnd);
71
+ }
72
+
73
+ const [totalCount, byLabel, byDate, byUser] = await Promise.all([
74
+ Log.countDocuments(matchStage),
75
+
76
+ Log.aggregate([
77
+ { $match: matchStage },
78
+ { $group: { _id: "$label", count: { $sum: 1 } } },
79
+ { $sort: { count: -1 } },
80
+ ]),
81
+
82
+ Log.aggregate([
83
+ { $match: matchStage },
84
+ {
85
+ $group: {
86
+ _id: {
87
+ $dateToString: {
88
+ format: "%Y-%m-%d",
89
+ date: { $toDate: "$createdAt" },
90
+ },
91
+ },
92
+ count: { $sum: 1 },
93
+ },
94
+ },
95
+ { $sort: { _id: 1 } },
96
+ ]),
97
+
98
+ Log.aggregate([
99
+ { $match: matchStage },
100
+ { $group: { _id: "$_user", count: { $sum: 1 } } },
101
+ {
102
+ $lookup: {
103
+ from: "users",
104
+ localField: "_id",
105
+ foreignField: "_id",
106
+ as: "user",
107
+ },
108
+ },
109
+ { $unwind: "$user" },
110
+ {
111
+ $project: {
112
+ name: { $concat: ["$user.name", " ", "$user.lastName"] },
113
+ count: 1,
114
+ },
115
+ },
116
+ { $sort: { count: -1 } },
117
+ ]),
118
+ ]);
119
+
120
+ const topUsers = byUser.slice(0, 10);
121
+ const leastUsers = [...byUser]
122
+ .sort((a, b) => a.count - b.count)
123
+ .slice(0, 10);
65
124
 
66
125
  // for (let i in consulta) {
67
126
  // consulta[i].label = consulta[i]._label.label;
@@ -69,14 +128,36 @@ self.retrieve = async (req, res) => {
69
128
  // }
70
129
 
71
130
  const response = {
72
- dataset0: { field: "Visualizaciones totales", count: consulta.length },
73
- dataset1: processDataset1(consulta),
74
- dataset2: processDataset2(consulta),
75
- dataset3: processDataset3(consulta),
76
- dataset4: processDataset4(consulta),
77
- dataset5: processDataset5(consulta),
78
- dataset6: processDataset6(consulta),
79
- dataset7: processDataset7(consulta),
131
+ dataset0: { field: "Visualizaciones totales", count: totalCount },
132
+ dataset1: [],
133
+ dataset2: {
134
+ field: "Distribución de acciones",
135
+ counts: byLabel.map((i) => i.count),
136
+ operations: byLabel.map((i) => i._id),
137
+ },
138
+ dataset3: {
139
+ field: "Distribución de acciones",
140
+ items: byLabel.map((i) => ({
141
+ addGroup: i._id,
142
+ totalResponse: i.count,
143
+ })),
144
+ },
145
+ dataset4: {
146
+ field: "Actividad en la plataforma",
147
+ counts: byDate.map((i) => i.count),
148
+ actionsName: byDate.map((i) => formatDate(i._id)),
149
+ },
150
+ dataset5: [],
151
+ dataset6: {
152
+ field: "Usuarios con mas actividad en la plataforma",
153
+ counts: topUsers.map((i) => i.count),
154
+ actionsName: topUsers.map((i) => i.name.split(" ")),
155
+ },
156
+ dataset7: {
157
+ field: "Usuarios con menos actividad en la plataforma",
158
+ counts: leastUsers.map((i) => i.count),
159
+ actionsName: leastUsers.map((i) => i.name.split(" ")),
160
+ },
80
161
  };
81
162
 
82
163
  res.status(200).send(response);
@@ -86,8 +167,8 @@ self.retrieve = async (req, res) => {
86
167
  }
87
168
  };
88
169
 
89
- function formatDate(date) {
90
- const day = String(date.getDate()).padStart(2, "0");
170
+ function formatDate(isoDate) {
171
+ const [year, month, day] = isoDate.split("-");
91
172
  const monthNames = [
92
173
  "Ene",
93
174
  "Feb",
@@ -102,10 +183,9 @@ function formatDate(date) {
102
183
  "Nov",
103
184
  "Dic",
104
185
  ];
105
- const month = monthNames[date.getMonth()];
106
- const year = date.getFullYear();
107
- return `${day} ${month} ${year}`;
186
+ return `${day} ${monthNames[parseInt(month) - 1]} ${year}`;
108
187
  }
188
+
109
189
  function processDataset1(consulta) {
110
190
  return consulta.map((item) => {
111
191
  return {
@@ -277,4 +357,4 @@ self.count = async (req, res) => {
277
357
  } catch (error) {
278
358
  res.status(400).send({ error: error.message });
279
359
  }
280
- };
360
+ };
@@ -0,0 +1,46 @@
1
+ const Totp = require("../services/totp");
2
+ const utils = require("../config/utils");
3
+
4
+ const self = module.exports;
5
+
6
+ self.setup = async (req, res) => {
7
+ try {
8
+ const tempToken = req.query.tempToken || req.body?.tempToken;
9
+ const response = await Totp.setup(tempToken);
10
+ res.status(200).send(response);
11
+ } catch (error) {
12
+ await utils.responseError(res, error);
13
+ }
14
+ };
15
+
16
+ self.activate = async (req, res) => {
17
+ try {
18
+ const tempToken = req.headers["x-temp-token"] || req.body?.tempToken;
19
+ const response = await Totp.activate(tempToken, req.body.token);
20
+ res.status(200).send(response);
21
+ } catch (error) {
22
+ await utils.responseError(res, error);
23
+ }
24
+ };
25
+
26
+ self.checkLogin = async (req, res) => {
27
+ try {
28
+ const { token, tempToken } = req.body;
29
+ if (!token || !tempToken)
30
+ return res.status(400).send({ message: "Faltan parámetros" });
31
+
32
+ const response = await Totp.checkLogin(token, tempToken, res);
33
+ res.status(200).send(response);
34
+ } catch (error) {
35
+ await utils.responseError(res, error);
36
+ }
37
+ };
38
+
39
+ self.adminToggle = async (req, res) => {
40
+ try {
41
+ const response = await Totp.adminToggle(req.params.USER_ID, req.body.enabled);
42
+ res.status(200).send(response);
43
+ } catch (error) {
44
+ await utils.responseError(res, error);
45
+ }
46
+ };
@@ -6,23 +6,25 @@ const jwt = require("jsonwebtoken");
6
6
  const dayjs = require("dayjs");
7
7
  const serviceUser = require("../services/user");
8
8
  const utils = require("../config/utils");
9
+ const _brand = utils.brand;
9
10
  const mongoose = require("mongoose");
10
11
  const AWS_SES = require("../services/ses");
11
12
  const Business = require("../models/Business");
12
13
  const self = module.exports;
13
14
 
14
15
  self.create = async (req, res) => {
15
- // Create a new user
16
16
  try {
17
17
  let user = await serviceUser.create(req.body);
18
- // Send email to user
19
18
  if (process.env.SEND_EMAIL_USER === "true") {
20
- let file = fs.readFileSync(process.env.TEMPLATE_ACCOUNT, "utf8");
19
+ let file = _brand.template("TEMPLATE_ACCOUNT");
21
20
  file = file.replace("{{user}}", user.name);
22
21
  file = file.replace("{{email}}", req.body.email);
23
22
  file = file.replace("{{password}}", req.body.pwd);
24
- if (process.env.URL_EMAIL) {
25
- file = file.replace("{{urlToken}}", process.env.URL_EMAIL);
23
+ const app = process.env.APP;
24
+ const urlEmail = process.env[`URL_EMAIL_${app}`];
25
+
26
+ if (urlEmail) {
27
+ file = file.replaceAll("{{urlToken}}", urlEmail);
26
28
  }
27
29
  await AWS_SES.sendCustom(
28
30
  user.email,
@@ -30,16 +32,9 @@ self.create = async (req, res) => {
30
32
  process.env.SUBJECT_EMAIL || "bienvenido"
31
33
  );
32
34
  }
33
-
34
35
  res.status(201).send(user);
35
36
  } catch (error) {
36
- utils.responseError(
37
- res,
38
- error,
39
- 400,
40
- "Error al crear usuario",
41
- "Revisa el detalle del error"
42
- );
37
+ utils.responseError(res, error, 400, "Error al crear usuario", "Revisa el detalle del error");
43
38
  }
44
39
  };
45
40
 
@@ -77,22 +72,20 @@ self.status = async (req, resp) => {
77
72
 
78
73
  self.updatepassword = async (req, resp) => {
79
74
  try {
80
- const result = await serviceUser.updatepassword(
81
- req.body,
82
- req.params.USER_ID
83
- );
84
-
85
- if(req.body.sendEmail === true){
86
-
75
+ const result = await serviceUser.updatepassword(req.body, req.params.USER_ID);
76
+ if (req.body.sendEmail === true) {
87
77
  const _id = req.params.USER_ID;
88
78
  let user = await User.findOne({ _id }, { pwd: 0 }).lean();
89
79
  if (process.env.SEND_EMAIL_USER === "true") {
90
- let file = fs.readFileSync(process.env.TEMPLATE_ACCOUNT, "utf8");
91
- file = file.replace("{{user}}", req.user.name);
92
- file = file.replace("{{email}}", req.user.email);
80
+ let file = _brand.template("TEMPLATE_ACCOUNT");
81
+ file = file.replace("{{user}}", user.name);
82
+ file = file.replace("{{email}}", user.email);
93
83
  file = file.replace("{{password}}", req.body.pwd);
94
- if (process.env.URL_EMAIL) {
95
- file = file.replace("{{urlToken}}", process.env.URL_EMAIL);
84
+ const app = process.env.APP;
85
+ const urlEmail = process.env[`URL_EMAIL_${app}`];
86
+
87
+ if (urlEmail) {
88
+ file = file.replaceAll("{{urlToken}}", urlEmail);
96
89
  }
97
90
  await AWS_SES.sendCustom(
98
91
  user.email,
@@ -101,7 +94,6 @@ self.updatepassword = async (req, resp) => {
101
94
  );
102
95
  }
103
96
  }
104
-
105
97
  resp.status(200).send(result);
106
98
  } catch (error) {
107
99
  resp.status(400).send({ error: error.message });
@@ -603,16 +595,10 @@ self.generatecode = async () => {
603
595
  self.sendcodemail = async (email, code) => {
604
596
  try {
605
597
  let user = await User.findOne({ email: email }, { name: 1, email: 1 });
606
-
607
- let file = fs.readFileSync(process.env.TEMPLATE_RECOVER_PASSWORD, "utf8");
598
+ let file = _brand.template("TEMPLATE_RECOVER_PASSWORD");
608
599
  file = file.replace("+++user+++", user.name);
609
600
  file = file.replace("+++code+++", code);
610
-
611
- return await alouxAWS.sendCustom(
612
- user.email,
613
- file,
614
- "Código de recuperación de contraseña"
615
- );
601
+ return await alouxAWS.sendCustom(user.email, file, "Código de recuperación de contraseña");
616
602
  } catch (error) {
617
603
  throw new Error("Ocurrio un error al envìar el correo electronico");
618
604
  }
@@ -829,18 +815,16 @@ self.validatePhone = async (req, res) => {
829
815
  self.sendverifyToken = async (correo, token) => {
830
816
  try {
831
817
  let user = await User.findOne({ email: correo }, { name: 1, email: 1 });
832
-
833
- let template = fs.readFileSync(process.env.TEMPLATE_VERIFY_EMAIL, "utf8");
818
+ let template = _brand.template("TEMPLATE_VERIFY_EMAIL");
834
819
  template = template.replaceAll("{{name}}", user.name);
835
820
  template = template.replaceAll(
836
821
  "{{urlVerifyEmail}}",
837
822
  process.env.URL_VERIFY_EMAIL + "/?token=" + token
838
823
  );
839
-
840
824
  return await alouxAWS.sendCustom(
841
825
  user.email,
842
826
  template,
843
- "Verifica tu cuenta de " + process.env.PROJECT_NAME
827
+ "Verifica tu cuenta de " + _brand.name()
844
828
  );
845
829
  } catch (error) {
846
830
  throw new Error("Ocurrio un error al envìar el correo electronico");
@@ -897,14 +881,12 @@ self.sendVerifyMailAccountJob = async (data, ban) => {
897
881
  self.sendValidateEmail = async (email) => {
898
882
  try {
899
883
  let user = await User.findOne({ email: email }, { name: 1, email: 1 });
900
-
901
- let file = fs.readFileSync(process.env.TEMPLATE_WELCOME, "utf8");
884
+ let file = _brand.template("TEMPLATE_WELCOME");
902
885
  file = file.replace("+++user+++", user.name);
903
-
904
- return await sesSDK.sendCustom(
886
+ return await AWS_SES.sendCustom(
905
887
  user.email,
906
888
  file,
907
- "Bienvenido a " + process.env.PROJECT_NAME
889
+ "Bienvenido a " + _brand.name()
908
890
  );
909
891
  } catch (error) {
910
892
  throw new Error("Ocurrio un error al envìar el correo electronico");
package/lib/router.js CHANGED
@@ -6,6 +6,8 @@ router.use(fileupload());
6
6
 
7
7
  const auth = require("./controllers/auth");
8
8
  const user = require("./controllers/user");
9
+ const totp = require("./controllers/totp");
10
+ const generatePassword = require("./controllers/generatePassword");
9
11
  const menu = require("./controllers/menu");
10
12
  const permission = require("./controllers/permission");
11
13
  const functions = require("./controllers/functions");
@@ -90,9 +92,18 @@ router.get("/iam/menu/count/all", middleware, menu.count);
90
92
  router.post("/iam/retrieve/history", middleware, history.retrieve);
91
93
  router.get("/iam/history/:HISTORY_ID", middleware, history.detail);
92
94
 
95
+ // IAM / GENERATE PASSWORD
96
+ router.get("/iam/generatePassword", generatePassword.generate);
97
+
93
98
  // Utilities
94
99
  router.patch("/iam/add/time/:TOKEN", user.addTimeToken);
95
100
 
101
+ // TOTP
102
+ router.post("/iam/totp/verify", totp.checkLogin);
103
+ router.get("/iam/totp/setup", totp.setup);
104
+ router.post("/iam/totp/activate", totp.activate);
105
+ router.put("/iam/user/:USER_ID/totp", middleware, totp.adminToggle);
106
+
96
107
  // IAM / Label
97
108
  router.post("/iam/label", middleware, label.create);
98
109
  router.patch("/iam/label/:LABEL_ID", middleware, label.update);
@@ -136,6 +147,8 @@ router.patch(
136
147
  business.favicon
137
148
  );
138
149
  router.get("/iam/business/:ID/identity", business.identity);
150
+ router.patch("/iam/business/:BUSINESS_ID/useCompanyKey", middleware, business.setUseCompanyKey);
151
+ router.post("/iam/business/:BUSINESS_ID/inheritKey", middleware, business.inheritKey);
139
152
 
140
153
  //Company
141
154
  router.post("/iam/company", middleware, company.create);
@@ -147,6 +160,8 @@ router.delete("/iam/company/:COMPANY_ID", middleware, company.delete);
147
160
  router.patch("/iam/company/:COMPANY_ID/picture", middleware, company.picture);
148
161
  router.patch("/iam/company/:COMPANY_ID/favicon", middleware, company.favicon);
149
162
  router.get("/iam/company/:ID/identity", company.identity);
163
+ router.put("/iam/company/:COMPANY_ID/gkey", middleware, company.updateGkey);
164
+ router.delete("/iam/company/:COMPANY_ID/gkey", middleware, company.deleteGkey);
150
165
 
151
166
  router.patch("/iam/company/:COMPANY_ID/picture", middleware, business.picture);
152
167
  router.patch("/iam/company/:COMPANY_ID/favicon", middleware, business.favicon);
@@ -8,6 +8,8 @@ const bigQuery = require("../services/bigQuery");
8
8
  const bcrypt = require("bcryptjs");
9
9
  const dayjs = require("dayjs");
10
10
  const fs = require("fs");
11
+ const utils = require("../config/utils");
12
+ const _brand = utils.brand;
11
13
  const jwt = require("jsonwebtoken");
12
14
  const mongoose = require("mongoose");
13
15
 
@@ -86,6 +88,7 @@ self.login = async (body, res) => {
86
88
  status: 1,
87
89
  }
88
90
  ).populate({ path: "_functions", select: { name: 1 } });
91
+
89
92
  if (!userLogin) {
90
93
  throw {
91
94
  code: 401,
@@ -95,6 +98,7 @@ self.login = async (body, res) => {
95
98
  error: new Error(),
96
99
  };
97
100
  }
101
+
98
102
  const token = await userLogin.generateAuthToken();
99
103
 
100
104
  res.cookie("token", token, {
@@ -123,10 +127,8 @@ self.login = async (body, res) => {
123
127
  throw {
124
128
  code: 401,
125
129
  title: "Límite de sesiones alcanzado",
126
- detail:
127
- "Has alcanzado el número máximo de sesiones permitidas para esta cuenta.",
128
- suggestion:
129
- "Por favor, cierra una de las sesiones activas en dispositivos que no estés usando para iniciar una nueva sesión.",
130
+ detail: "Has alcanzado el número máximo de sesiones permitidas para esta cuenta.",
131
+ suggestion: "Por favor, cierra una de las sesiones activas en dispositivos que no estés usando para iniciar una nueva sesión.",
130
132
  error: new Error(),
131
133
  };
132
134
  }
@@ -145,10 +147,8 @@ self.login = async (body, res) => {
145
147
  throw {
146
148
  code: 401,
147
149
  title: "Usuario bloqueado",
148
- detail:
149
- "Tu cuenta ha sido bloqueada debido a múltiples intentos fallidos de inicio de sesión.",
150
- suggestion:
151
- "Para reactivar tu cuenta, utiliza la opción de recuperación de contraseña e intenta nuevamente.",
150
+ detail: "Tu cuenta ha sido bloqueada debido a múltiples intentos fallidos de inicio de sesión.",
151
+ suggestion: "Para reactivar tu cuenta, utiliza la opción de recuperación de contraseña e intenta nuevamente.",
152
152
  error: new Error(),
153
153
  status: userLogin.status,
154
154
  };
@@ -158,7 +158,6 @@ self.login = async (body, res) => {
158
158
  const isPasswordMatch = await bcrypt.compare(pwd, userLogin.pwd);
159
159
 
160
160
  if (!isPasswordMatch) {
161
- //conteo de inicios fallidos
162
161
  if (userLogin.validateKey.failedAttempts === process.env.FAILED_ATTEMPS) {
163
162
  await User.updateOne({ _id: userLogin._id }, { status: "Bloqueado" });
164
163
  } else {
@@ -175,8 +174,23 @@ self.login = async (body, res) => {
175
174
  error: new Error(),
176
175
  };
177
176
  } else {
177
+ // ── TOTP: si tiene 2FA activo, no emitir token real todavía ──────────────
178
+ if (userLogin?.data?.totp?.enabled === true) {
179
+ const tempToken = jwt.sign(
180
+ { _id: userLogin._id, type: "totp_pending" },
181
+ process.env.AUTH_SECRET,
182
+ { expiresIn: "5m" }
183
+ );
184
+
185
+ const needsSetup = !userLogin?.data?.totp?.secret;
186
+
187
+ return { requires2FA: true, needsSetup, tempToken };
188
+ }
189
+
190
+ // ── Flujo normal sin TOTP ─────────────────────────────────────────────────
178
191
  const token = await userLogin.generateAuthToken();
179
192
  let changePwd;
193
+
180
194
  if (!userLogin?.data) {
181
195
  userLogin.data.changePwd = false;
182
196
  changePwd = false;
@@ -453,34 +467,24 @@ self.generatecode = async () => {
453
467
  };
454
468
 
455
469
  self.sendcodemail = async (email, code, req) => {
456
- let file;
457
- const user = await User.findOne({ email: email }, { name: 1, email: 1 });
458
- if (!req.body.enviroment) {
459
- file = fs.readFileSync(process.env.TEMPLATE_RECOVER_PASSWORD, "utf8");
460
- } else {
461
- file = fs.readFileSync(process.env[req.body.enviroment], "utf8");
462
- }
470
+ const user = await User.findOne({ email }, { name: 1, email: 1 });
471
+ let file = !req.body.enviroment
472
+ ? _brand.template("TEMPLATE_RECOVER_PASSWORD")
473
+ : _brand.template(req.body.enviroment);
463
474
  file = file.replace("+++user+++", user.name);
464
475
  file = file.replace("+++code+++", code);
465
- await ses.sendCustom(
466
- user.email,
467
- file,
468
- "Código de recuperación de contraseña"
469
- );
470
-
476
+ await ses.sendCustom(user.email, file, "Código de recuperación de contraseña");
471
477
  return true;
472
478
  };
473
479
 
474
480
  self.sendcodemailLogin = async (email, code, ban) => {
475
- let file;
481
+ let file = _brand.template("TEMPLATE_LOGIN_CODE");
476
482
  if (ban === true) {
477
- file = fs.readFileSync(process.env.TEMPLATE_LOGIN_CODE, "utf8");
478
483
  file = file.replace("+++user+++", email);
479
484
  file = file.replace("+++code+++", code);
480
485
  await ses.sendCustom(email, file, "Código inicio de sesión");
481
486
  } else {
482
- const user = await User.findOne({ email: email }, { name: 1, email: 1 });
483
- file = fs.readFileSync(process.env.TEMPLATE_LOGIN_CODE, "utf8");
487
+ const user = await User.findOne({ email }, { name: 1, email: 1 });
484
488
  file = file.replace("+++user+++", user.name);
485
489
  file = file.replace("+++code+++", code);
486
490
  await ses.sendCustom(user.email, file, "Código inicio de sesión");
@@ -649,16 +653,10 @@ self.resetPassword = async (req, res) => {
649
653
 
650
654
  self.sendverifyToken = async (correo, token) => {
651
655
  let user = await User.findOne({ email: correo }, { name: 1, email: 1 });
652
-
653
- let file = fs.readFileSync(process.env.TEMPLATE_VERIFY_EMAIL, "utf8");
656
+ let file = _brand.template("TEMPLATE_VERIFY_EMAIL");
654
657
  file = file.replace("+++user+++", user.name);
655
658
  file = file.replace("+++token+++", token);
656
-
657
- await ses.sendCustom(
658
- user.email,
659
- file,
660
- "Verifica tu cuenta de " + process.env.PROJECT_NAME
661
- );
659
+ await ses.sendCustom(user.email, file, "Verifica tu cuenta de " + _brand.name());
662
660
  return true;
663
661
  };
664
662
 
@@ -686,16 +684,10 @@ self.sendVerifyMailAccountJob = async (data, ban) => {
686
684
  };
687
685
 
688
686
  self.sendValidateEmail = async (email) => {
689
- let user = await User.findOne({ email: email }, { name: 1, email: 1 });
690
-
691
- let file = fs.readFileSync(process.env.TEMPLATE_WELCOME, "utf8");
687
+ let user = await User.findOne({ email }, { name: 1, email: 1 });
688
+ let file = _brand.template("TEMPLATE_WELCOME");
692
689
  file = file.replace("+++user+++", user.name);
693
-
694
- return await ses.sendCustom(
695
- user.email,
696
- file,
697
- "Bienvenido a " + process.env.PROJECT_NAME
698
- );
690
+ return await ses.sendCustom(user.email, file, "Bienvenido a " + _brand.name());
699
691
  };
700
692
 
701
693
  self.verifyMailTokenAccount = async (req, res) => {
@@ -896,23 +888,19 @@ self.createCustomer = async (req, res) => {
896
888
 
897
889
  self.mailChange = async (newEmail, user) => {
898
890
  const code = await self.generatecode();
899
-
900
891
  let time = new Date();
901
892
  const sumarMinutos = new Date(time.getTime() + 5 * 60000);
902
893
  await User.updateOne(
903
- {
904
- _id: user._id,
905
- },
894
+ { _id: user._id },
906
895
  {
907
896
  "validateKey.resetPassword.resetCode": code,
908
897
  "validateKey.limitCodeTime": new Date(sumarMinutos).getTime(),
909
898
  }
910
899
  );
911
- let file = fs.readFileSync(process.env.TEMPLATE_CHANGE_MAIL, "utf8");
900
+ let file = _brand.template("TEMPLATE_CHANGE_MAIL");
912
901
  file = file.replace("+++user+++", user.name);
913
902
  file = file.replace("+++code+++", code);
914
903
  await ses.sendCustom(newEmail, file, "Código para cambio de correo");
915
-
916
904
  return true;
917
905
  };
918
906
 
@@ -0,0 +1,44 @@
1
+ const self = module.exports;
2
+
3
+ self.generatePassword = (length = 12) => {
4
+ const lowercase = "abcdefghijklmnopqrstuvwxyz";
5
+ const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
6
+ const numbers = "0123456789";
7
+ const special = "!@#$%&*";
8
+ const allChars = lowercase + uppercase + numbers + special;
9
+
10
+ let availableChars = allChars.split("");
11
+ let password = "";
12
+
13
+ const getRandomChar = (chars) => {
14
+ const index = Math.floor(Math.random() * chars.length);
15
+ const char = chars[index];
16
+ availableChars = availableChars.filter((c) => c !== char);
17
+ return char;
18
+ };
19
+
20
+ password += getRandomChar(
21
+ lowercase.split("").filter((c) => availableChars.includes(c)),
22
+ );
23
+ password += getRandomChar(
24
+ uppercase.split("").filter((c) => availableChars.includes(c)),
25
+ );
26
+ password += getRandomChar(
27
+ numbers.split("").filter((c) => availableChars.includes(c)),
28
+ );
29
+ password += getRandomChar(
30
+ special.split("").filter((c) => availableChars.includes(c)),
31
+ );
32
+
33
+ for (let i = password.length; i < length; i++) {
34
+ if (availableChars.length === 0) break;
35
+ const index = Math.floor(Math.random() * availableChars.length);
36
+ password += availableChars[index];
37
+ availableChars.splice(index, 1);
38
+ }
39
+
40
+ return password
41
+ .split("")
42
+ .sort(() => Math.random() - 0.5)
43
+ .join("");
44
+ };
@@ -0,0 +1,169 @@
1
+ const speakeasy = require("speakeasy");
2
+ const QRCode = require("qrcode");
3
+ const jwt = require("jsonwebtoken");
4
+ const dayjs = require("dayjs");
5
+ const User = require("../models/User");
6
+
7
+ const self = module.exports;
8
+
9
+ // Helper
10
+
11
+ const verifyTotpToken = (secret, token) =>
12
+ speakeasy.totp.verify({ secret, encoding: "base32", token, window: 1 });
13
+
14
+ const verifyTempToken = (tempToken) => {
15
+ let payload;
16
+ try {
17
+ payload = jwt.verify(tempToken, process.env.AUTH_SECRET);
18
+ if (payload.type !== "totp_pending") throw new Error();
19
+ } catch {
20
+ throw {
21
+ code: 401,
22
+ title: "Sesión expirada.",
23
+ detail: "",
24
+ suggestion: "Vuelve a iniciar sesión",
25
+ error: new Error(),
26
+ };
27
+ }
28
+ return payload;
29
+ };
30
+
31
+ // genera QR y guarda el secreto
32
+ self.setup = async (tempToken) => {
33
+ const payload = verifyTempToken(tempToken);
34
+
35
+ const user = await User.findOne({ _id: payload._id });
36
+
37
+ if (!user)
38
+ throw {
39
+ code: 404,
40
+ title: "Usuario no encontrado.",
41
+ detail: "",
42
+ suggestion: "",
43
+ error: new Error(),
44
+ };
45
+
46
+ if (!user?.data?.totp?.enabled)
47
+ throw {
48
+ code: 403,
49
+ title: "2FA no habilitado.",
50
+ detail: "",
51
+ suggestion: "Contacta al administrador para habilitarlo",
52
+ error: new Error(),
53
+ };
54
+
55
+ const secret = speakeasy.generateSecret({
56
+ name: `${process.env.PROJECT_NAME} (${user.email})`,
57
+ issuer: process.env.PROJECT_NAME,
58
+ length: 20,
59
+ });
60
+
61
+ const qrCode = await QRCode.toDataURL(secret.otpauth_url);
62
+
63
+ await User.updateOne(
64
+ { _id: user._id },
65
+ { $set: { "data.totp.secret": secret.base32 } },
66
+ );
67
+
68
+ return { secret: secret.base32, qrCode };
69
+ };
70
+
71
+ // valida el código tras escanear el QR
72
+ self.activate = async (tempToken, token) => {
73
+ const payload = verifyTempToken(tempToken);
74
+
75
+ const user = await User.findOne({ _id: payload._id });
76
+
77
+ if (!user?.data?.totp?.secret)
78
+ throw {
79
+ code: 400,
80
+ title: "QR no generado.",
81
+ detail: "",
82
+ suggestion: "Primero genera el QR desde /totp/setup",
83
+ error: new Error(),
84
+ };
85
+
86
+ if (!verifyTotpToken(user.data.totp.secret, token))
87
+ throw {
88
+ code: 400,
89
+ title: "Código inválido.",
90
+ detail: "",
91
+ suggestion:
92
+ "El código ingresado es incorrecto, verifica que sea el correcto e intenta de nuevo.",
93
+ error: new Error(),
94
+ };
95
+
96
+ return { message: "TOTP configurado correctamente" };
97
+ };
98
+
99
+ // valida código en login y emite token real
100
+ self.checkLogin = async (token, tempToken, res) => {
101
+ const payload = verifyTempToken(tempToken);
102
+
103
+ const user = await User.findOne({ _id: payload._id });
104
+
105
+ if (!user?.data?.totp?.enabled)
106
+ throw {
107
+ code: 400,
108
+ title: "2FA no está configurado.",
109
+ detail: "",
110
+ suggestion: "",
111
+ error: new Error(),
112
+ };
113
+
114
+ if (!user?.data?.totp?.secret)
115
+ throw {
116
+ code: 400,
117
+ title: "TOTP no configurado.",
118
+ detail: "",
119
+ suggestion: "Configura tu app Authenticator primero",
120
+ error: new Error(),
121
+ };
122
+
123
+ if (!verifyTotpToken(user.data.totp.secret, token))
124
+ throw {
125
+ code: 401,
126
+ title: "Código inválido o expirado.",
127
+ detail: "",
128
+ suggestion:
129
+ "El código ingresado es incorrecto, verifica que sea el correcto e intenta de nuevo.",
130
+ error: new Error(),
131
+ };
132
+
133
+ user.data.changePwd = user?.data?.changePwd ?? false;
134
+ user.validateKey.failedAttempts = 0;
135
+ await user.save();
136
+
137
+ const authToken = await user.generateAuthToken();
138
+
139
+ res.cookie("token", authToken, {
140
+ secure: true,
141
+ httpOnly: true,
142
+ sameSite: "none",
143
+ expires: dayjs().add(30, "days").toDate(),
144
+ });
145
+
146
+ return { token: authToken, changePwd: user.data.changePwd };
147
+ };
148
+
149
+ // admin habilita o deshabilita 2FA de un usuario
150
+ self.adminToggle = async (userId, enabled) => {
151
+ const user = await User.findOne({ _id: userId });
152
+
153
+ if (!user)
154
+ throw {
155
+ code: 404,
156
+ title: "Usuario no encontrado.",
157
+ detail: "",
158
+ suggestion: "",
159
+ error: new Error(),
160
+ };
161
+
162
+ const update = enabled
163
+ ? { "data.totp.enabled": true }
164
+ : { "data.totp.enabled": false, "data.totp.secret": null };
165
+
166
+ await User.updateOne({ _id: user._id }, { $set: update });
167
+
168
+ return { message: enabled ? "2FA habilitado" : "2FA deshabilitado" };
169
+ };
package/lib/swagger.yaml CHANGED
@@ -1389,4 +1389,212 @@ paths:
1389
1389
  itemsPerPage: 10
1390
1390
  responses:
1391
1391
  '200':
1392
- description: ok
1392
+ description: ok
1393
+ # Generar contraseña
1394
+ /iam/generatePassword:
1395
+ get:
1396
+ summary: Generar contraseña segura
1397
+ tags:
1398
+ - utilities
1399
+ description: >
1400
+ Genera una contraseña aleatoria segura. Garantiza al menos una letra
1401
+ minúscula, una mayúscula, un número y un carácter especial (!@#$%&*).
1402
+ No requiere autenticación.
1403
+ parameters:
1404
+ - name: length
1405
+ in: query
1406
+ required: false
1407
+ description: Longitud de la contraseña (default 12)
1408
+ schema:
1409
+ type: integer
1410
+ default: 12
1411
+ minimum: 4
1412
+ responses:
1413
+ '200':
1414
+ description: Contraseña generada
1415
+ content:
1416
+ application/json:
1417
+ schema:
1418
+ type: object
1419
+ properties:
1420
+ password:
1421
+ type: string
1422
+ example: "aB3!xKm9Zq#T"
1423
+ # TOTP
1424
+ /iam/totp/setup:
1425
+ get:
1426
+ summary: Generar QR y secreto TOTP
1427
+ tags:
1428
+ - totp
1429
+ description: >
1430
+ Genera un secreto TOTP y retorna un QR en base64 para escanear con una
1431
+ app Authenticator (Google Authenticator, Authy, etc.).
1432
+ Requiere un token temporal de tipo `totp_pending` obtenido tras el login
1433
+ cuando el usuario tiene 2FA habilitado.
1434
+ El secreto se guarda en el usuario hasta que sea activado con `/totp/activate`.
1435
+ parameters:
1436
+ - name: Authorization
1437
+ in: header
1438
+ required: true
1439
+ description: Bearer {tempToken} — token temporal tipo totp_pending
1440
+ schema:
1441
+ type: string
1442
+ responses:
1443
+ '200':
1444
+ description: Secreto y QR generados correctamente
1445
+ content:
1446
+ application/json:
1447
+ schema:
1448
+ type: object
1449
+ properties:
1450
+ secret:
1451
+ type: string
1452
+ description: Secreto en base32
1453
+ example: "JBSWY3DPEHPK3PXP"
1454
+ qrCode:
1455
+ type: string
1456
+ description: Imagen QR en formato Data URL (base64)
1457
+ example: "data:image/png;base64,iVBORw0KGgo..."
1458
+ '401':
1459
+ description: Token temporal expirado o inválido
1460
+ '403':
1461
+ description: El usuario no tiene 2FA habilitado
1462
+ '404':
1463
+ description: Usuario no encontrado
1464
+
1465
+ /iam/totp/activate:
1466
+ post:
1467
+ summary: Activar TOTP tras escanear el QR
1468
+ tags:
1469
+ - totp
1470
+ description: >
1471
+ Valida el código OTP generado por la app Authenticator para confirmar
1472
+ que el QR fue escaneado correctamente. Debe llamarse después de `/totp/setup`.
1473
+ Requiere token temporal tipo `totp_pending`.
1474
+ parameters:
1475
+ - name: Authorization
1476
+ in: header
1477
+ required: true
1478
+ description: Bearer {tempToken} — token temporal tipo totp_pending
1479
+ schema:
1480
+ type: string
1481
+ requestBody:
1482
+ content:
1483
+ application/json:
1484
+ schema:
1485
+ properties:
1486
+ token:
1487
+ description: Código OTP de 6 dígitos generado por la app Authenticator
1488
+ type: string
1489
+ example: "123456"
1490
+ required:
1491
+ - token
1492
+ responses:
1493
+ '200':
1494
+ description: TOTP configurado correctamente
1495
+ content:
1496
+ application/json:
1497
+ schema:
1498
+ type: object
1499
+ properties:
1500
+ message:
1501
+ type: string
1502
+ example: "TOTP configurado correctamente"
1503
+ '400':
1504
+ description: QR no generado previamente o código inválido
1505
+ '401':
1506
+ description: Token temporal expirado o inválido
1507
+
1508
+ /iam/totp/verify:
1509
+ post:
1510
+ summary: Verificar código TOTP en el login
1511
+ tags:
1512
+ - totp
1513
+ description: >
1514
+ Segundo paso del login cuando el usuario tiene 2FA activo.
1515
+ Valida el código OTP y, si es correcto, emite el token de sesión real
1516
+ y lo setea como cookie `httpOnly`.
1517
+ parameters:
1518
+ - name: Authorization
1519
+ in: header
1520
+ required: true
1521
+ description: Bearer {tempToken} — token temporal tipo totp_pending
1522
+ schema:
1523
+ type: string
1524
+ requestBody:
1525
+ content:
1526
+ application/json:
1527
+ schema:
1528
+ properties:
1529
+ token:
1530
+ description: Código OTP de 6 dígitos generado por la app Authenticator
1531
+ type: string
1532
+ example: "123456"
1533
+ required:
1534
+ - token
1535
+ responses:
1536
+ '200':
1537
+ description: Login completado, retorna token de sesión
1538
+ content:
1539
+ application/json:
1540
+ schema:
1541
+ type: object
1542
+ properties:
1543
+ token:
1544
+ type: string
1545
+ description: JWT de sesión
1546
+ example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
1547
+ changePwd:
1548
+ type: boolean
1549
+ description: Indica si el usuario debe cambiar su contraseña
1550
+ example: false
1551
+ '400':
1552
+ description: 2FA no configurado o secreto faltante
1553
+ '401':
1554
+ description: Código inválido o expirado / token temporal inválido
1555
+
1556
+ /iam/user/{USER_ID}/totp:
1557
+ put:
1558
+ summary: Habilitar o deshabilitar 2FA de un usuario (admin)
1559
+ tags:
1560
+ - totp
1561
+ description: >
1562
+ Permite a un administrador habilitar o deshabilitar el 2FA de cualquier usuario.
1563
+ Al deshabilitar, también elimina el secreto TOTP guardado.
1564
+ security:
1565
+ - bearerAuth: []
1566
+ parameters:
1567
+ - name: USER_ID
1568
+ in: path
1569
+ required: true
1570
+ description: ID del usuario a modificar
1571
+ schema:
1572
+ type: string
1573
+ format: ObjectId
1574
+ example: "64371ab1b6dc1417c9c0b812"
1575
+ requestBody:
1576
+ content:
1577
+ application/json:
1578
+ schema:
1579
+ properties:
1580
+ enabled:
1581
+ type: boolean
1582
+ description: >
1583
+ `true` para habilitar 2FA, `false` para deshabilitar
1584
+ (también borra el secreto)
1585
+ example: true
1586
+ required:
1587
+ - enabled
1588
+ responses:
1589
+ '200':
1590
+ description: Estado de 2FA actualizado
1591
+ content:
1592
+ application/json:
1593
+ schema:
1594
+ type: object
1595
+ properties:
1596
+ message:
1597
+ type: string
1598
+ example: "2FA habilitado"
1599
+ '404':
1600
+ description: Usuario no encontrado
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aloux-iam",
3
- "version": "0.0.139",
3
+ "version": "0.0.141",
4
4
  "description": "Aloux IAM for APIs ",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -33,6 +33,8 @@
33
33
  "method-override": "^3.0.0",
34
34
  "mongodb": "^4.17.x",
35
35
  "mongoose": "^6.12.x",
36
+ "qrcode": "^1.5.4",
37
+ "speakeasy": "^2.0.0",
36
38
  "yamljs": "^0.3.0"
37
39
  }
38
- }
40
+ }