backend-plus 2.5.2-betha.23 → 2.5.2-betha.25

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.
@@ -56,6 +56,7 @@ export interface ProcedureDef<T = any> {
56
56
  forExport?:{
57
57
  fileName?:string
58
58
  csvFileName?:string
59
+ generarInmediato?:boolean
59
60
  }
60
61
  }
61
62
 
@@ -247,7 +248,9 @@ export interface ForeignKey {
247
248
  onUpdate?: FkActions,
248
249
  onDelete?: FkActions,
249
250
  displayAllFields?: boolean,
250
- alias?:string,
251
+ alias?:string,
252
+ abr?:string,
253
+ label?:string,
251
254
  displayFields?:string[],
252
255
  consName?:string,
253
256
  initiallyDeferred?:boolean
@@ -83,7 +83,104 @@ function md5(text){
83
83
  return crypto.createHash('md5').update(text).digest('hex');
84
84
  }
85
85
 
86
- //
86
+ const DEFAULT_ITERATIONS = 4096;
87
+ const HASH_ALGORITHM = 'sha256';
88
+ const KEY_LENGTH = 64; // Longitud de la clave (64 bytes)
89
+ const bufferToBase64 = (buffer) => buffer.toString('base64');
90
+
91
+ /**
92
+ * Función central para PBKDF2.
93
+ * Devuelve la clave derivada.
94
+ */
95
+ function deriveKey(password, salt, iterations) {
96
+ return new Promise((resolve, reject) => {
97
+ crypto.pbkdf2(
98
+ password,
99
+ salt,
100
+ iterations,
101
+ KEY_LENGTH,
102
+ HASH_ALGORITHM,
103
+ (err, derivedKey) => {
104
+ if (err) return reject(err);
105
+ resolve(derivedKey);
106
+ }
107
+ );
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Genera la cadena de verificación SCRAM-SHA-256 en formato PostgreSQL.
113
+ * Formato: SCRAM-SHA-256$iterations:salt$verifier
114
+ * @param {string} password - Contraseña en texto plano.
115
+ * @returns {Promise<string>} La cadena SCRAM completa.
116
+ */
117
+ async function generateScramVerifier(password) {
118
+ // Genera Salt aleatoria
119
+ const saltBuffer = crypto.randomBytes(32);
120
+ const saltBase64 = bufferToBase64(saltBuffer);
121
+
122
+ // Deriva la clave (Verifier)
123
+ const verifierBuffer = await deriveKey(
124
+ password,
125
+ saltBase64,
126
+ DEFAULT_ITERATIONS
127
+ );
128
+ const verifierBase64 = bufferToBase64(verifierBuffer);
129
+
130
+ // Ensambla la cadena final (PostgreSQL style)
131
+ // Usa $ como delimitador principal (aunque PostgreSQL lo pone dentro de la tabla).
132
+ // El formato canónico es 'SCRAM-SHA-256$i:salt, verifier$encoded_verifier'
133
+ return `SCRAM-SHA-256$${DEFAULT_ITERATIONS}:${saltBase64}$${verifierBase64}`;
134
+ }
135
+
136
+ /**
137
+ * Verifica una contraseña en texto plano contra la cadena SCRAM almacenada.
138
+ * @param {string} password - Contraseña en texto plano ingresada por el usuario.
139
+ * @param {string} storedScramString - Cadena SCRAM completa (ej: SCRAM-SHA-256$4096:SALT_BASE64$VERIFIER_BASE64).
140
+ * @returns {Promise<boolean>} True si la contraseña es válida.
141
+ */
142
+ async function verifyScram(password, storedScramString) {
143
+ if (!storedScramString.startsWith('SCRAM-SHA-256$')) {
144
+ // No es formato SCRAM. Debe ser MD5 u otro algoritmo.
145
+ return false;
146
+ }
147
+
148
+ // Parsea la cadena SCRAM
149
+ // Ejemplo: SCRAM-SHA-256$4096:SALT_BASE64$VERIFIER_BASE64
150
+ const parts = storedScramString.split('$');
151
+ if (parts.length !== 3) {
152
+ throw new Error('Formato SCRAM almacenado inválido.');
153
+ }
154
+
155
+ // Obtiene iteraciones y salt de la segunda parte (ej: '4096:SALT_BASE64')
156
+ const [storedIterations, storedSalt] = parts[1].split(':');
157
+ const storedVerifier = parts[2];
158
+
159
+ if (!storedSalt || !storedVerifier || isNaN(parseInt(storedIterations))) {
160
+ throw new Error('Datos de SCRAM incompletos o malformados.');
161
+ }
162
+
163
+ const iterations = parseInt(storedIterations);
164
+
165
+ // Deriva la clave de la contraseña ingresada
166
+ const generatedVerifierBuffer = await deriveKey(
167
+ password,
168
+ storedSalt,
169
+ iterations
170
+ );
171
+ const generatedVerifier = bufferToBase64(generatedVerifierBuffer);
172
+
173
+ // Compara de manera segura contra el Verificador Almacenado
174
+ const storedVerifierBuffer = Buffer.from(storedVerifier, 'base64');
175
+ const generatedVerifierBufferFromBase64 = Buffer.from(generatedVerifier, 'base64');
176
+
177
+ // Usa crypto.timingSafeEqual para evitar ataques de temporización
178
+ if (generatedVerifierBufferFromBase64.length !== storedVerifierBuffer.length) {
179
+ return false;
180
+ }
181
+
182
+ return crypto.timingSafeEqual(generatedVerifierBufferFromBase64, storedVerifierBuffer);
183
+ }
87
184
 
88
185
  var dist=regexpDistCheck.test(packagejson.main)?'dist/':'';
89
186
 
@@ -1023,34 +1120,77 @@ AppBackend.prototype.start = function start(opts){
1023
1120
  }
1024
1121
  });
1025
1122
  mainApp.loginPlusManager.setValidatorStrategy(
1026
- function(req, username, password, done) {
1027
- var client;
1028
- if(!be.config.login["preserve-case"]){
1029
- username = username.toLowerCase().trim();
1030
- }
1031
- be.getDbClient(req).then(function(cli){
1032
- client = cli;
1033
- return client.query("CALL set_app_user('!login')").execute();
1034
- }).then(function(){
1035
- var infoFieldList=be.config.login.infoFieldList||(be.config.login.rolFieldName?[be.config.login.userFieldName,be.config.login.rolFieldName]:[be.config.login.userFieldName]);
1036
- return client.query(
1037
- "SELECT "+infoFieldList.map(function(fieldOrPair){ return fieldOrPair.split(' as ').map(function(ident){ return be.db.quoteIdent(ident)}).join(' as '); })+
1123
+ async function(req, username, password, done) {
1124
+ try{
1125
+ var client;
1126
+ if(!be.config.login["preserve-case"]){
1127
+ username = username.toLowerCase().trim();
1128
+ }
1129
+ client = await be.getDbClient(req);
1130
+ client.query("CALL set_app_user('!login')").execute();
1131
+ const {passFieldName, rolFieldName, userFieldName} = be.config.login;
1132
+ var infoFieldList=(
1133
+ be.config.login.infoFieldList||(
1134
+ rolFieldName?
1135
+ [userFieldName, rolFieldName]
1136
+ :
1137
+ [userFieldName]
1138
+ )
1139
+ ).concat(passFieldName);
1140
+
1141
+ const sql = "SELECT "+infoFieldList.map(function(fieldOrPair){ return fieldOrPair.split(' as ').map(function(ident){ return be.db.quoteIdent(ident)}).join(' as '); })+
1038
1142
  ", "+be.config.login.activeClausule+" as active "+
1039
1143
  ", "+be.config.login.lockedClausule+" as locked "+
1040
1144
  " FROM "+(be.config.login.from ?? (
1041
1145
  (be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':'')+
1042
1146
  be.db.quoteIdent(be.config.login.table)
1043
1147
  ))+
1044
- " WHERE "+be.db.quoteIdent(be.config.login.userFieldName)+" = $1 "+
1045
- " AND "+be.db.quoteIdent(be.config.login.passFieldName)+" = $2 ",
1046
- [username, md5(password+username)]
1047
- ).fetchOneRowIfExists();
1048
- }).then(function(data){
1148
+ " WHERE "+be.db.quoteIdent(be.config.login.userFieldName)+" = $1 ";
1149
+ const data = await client.query(sql,[username]).fetchOneRowIfExists();
1150
+
1151
+ if(data.rowCount != 1){
1152
+ done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1153
+ return
1154
+ }else{
1155
+ const user = data.row;
1156
+ const usaScramSha256 = user[passFieldName].startsWith('SCRAM-SHA-256$')
1157
+ if (usaScramSha256){
1158
+ const isScramValid = await verifyScram(
1159
+ password,
1160
+ user[passFieldName],
1161
+ )
1162
+ if(!isScramValid){
1163
+ done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1164
+ return
1165
+ }
1166
+ }else{
1167
+ if (md5(password+username.toLowerCase()) === user[passFieldName]) {
1168
+ console.log('Autenticación MD5 OK. Ejecutando migración a SCRAM...');
1169
+ // Generar nuevos valores SCRAM
1170
+ const hashPass = await generateScramVerifier(password);
1171
+ //sobrescribo el hash MD5 antiguo
1172
+ await client.query(`
1173
+ UPDATE usuarios
1174
+ SET ${be.db.quoteIdent(passFieldName)} = $1
1175
+ WHERE usuario = $2
1176
+ returning *`,
1177
+ [hashPass, username]
1178
+ ).fetchUniqueRow();
1179
+ console.log('Migración completada.');
1180
+ }else{
1181
+ done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1182
+ return
1183
+ }
1184
+ }
1185
+ }
1186
+ //continua validando
1049
1187
  if(data.rowCount==1){
1050
1188
  if(!data.row.active){
1051
1189
  done(null,false,{message:be.messages.unlogged.login.inactiveFail});
1190
+ return
1052
1191
  }else if(data.row.locked){
1053
1192
  done(null,false,{message:be.messages.unlogged.login.lockedFail});
1193
+ return
1054
1194
  }else{
1055
1195
  if(req.query["return-to"]){
1056
1196
  req.session.loginReturnTo = req.query["return-to"];
@@ -1058,12 +1198,13 @@ AppBackend.prototype.start = function start(opts){
1058
1198
  if(be.config.login["double-dragon"]){
1059
1199
  be.DoubleDragon.dbParams[username] = changing(be.config.db, {user:username, password});
1060
1200
  }
1061
- return data.row;
1062
1201
  }
1063
1202
  }else{
1064
1203
  done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1204
+ return
1065
1205
  }
1066
- }).then(async function(userInfo){
1206
+
1207
+ const userInfo = data.row;
1067
1208
  if (!userInfo) return;
1068
1209
  if (!be.config.login.skipBitacora) {
1069
1210
  var context = be.getContext(req);
@@ -1078,42 +1219,72 @@ AppBackend.prototype.start = function start(opts){
1078
1219
  userInfo.bitacoraId = sessionInfo.value;
1079
1220
  }
1080
1221
  done(null, userInfo);
1081
- }).then(function(){
1082
1222
  client.done();
1083
- }).catch(function(err){
1223
+ }catch(err){
1084
1224
  if(be.config.login["double-dragon"]){
1085
1225
  done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1086
1226
  }
1087
1227
  if(client && typeof client.done === "function"){
1088
1228
  client.done();
1089
1229
  }
1090
- throw err;
1091
- }).catch(function(err){
1092
1230
  console.log('login error',err);
1093
1231
  console.log(err.stack);
1094
1232
  done(new Error('internal login error'));
1095
- });
1233
+ };
1096
1234
  }
1097
1235
  );
1098
1236
  be.changePassword=async function(client,username,oldPassword,newPassword){
1099
- var sql = "UPDATE "+(be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':'')+
1100
- be.db.quoteIdent(be.config.login.table)+
1101
- "\n SET "+be.db.quoteIdent(be.config.login.passFieldName)+" = $2 "+
1102
- "\n WHERE "+be.db.quoteIdent(be.config.login.userFieldName)+" = $1 "+
1103
- (oldPassword!==false?"\n AND "+be.db.quoteIdent(be.config.login.passFieldName)+" = $3 ":"")+
1104
- "\n RETURNING 1 as ok";
1105
- var params = [username, md5(newPassword+username.toLowerCase())]
1106
- if(oldPassword!==false){
1107
- params.push(md5(oldPassword+username.toLowerCase()))
1237
+ const { table, passFieldName, userFieldName } = be.config.login;
1238
+ const { schema } = be.config.db;
1239
+ let ok = false;
1240
+ const data = await client.query(
1241
+ `SELECT *
1242
+ FROM ${(schema ? be.db.quoteIdent(schema) + '.' : '')}${be.db.quoteIdent(table)}
1243
+ WHERE ${be.db.quoteIdent(userFieldName)} = $1`,
1244
+ [username]
1245
+ ).fetchOneRowIfExists();
1246
+ if (data.rowCount !== 1) {
1247
+ // Usuario no encontrado
1248
+ return false;
1249
+ }
1250
+ const user = data.row
1251
+ const storedHash = user[passFieldName];
1252
+ if (oldPassword !== false) {
1253
+ if (storedHash.startsWith('SCRAM-SHA-256$')) {//Intento SCRAM-SHA-256
1254
+ ok = await verifyScram(oldPassword, storedHash);
1255
+ } else { //Intento MD5
1256
+ const md5Hash = md5(oldPassword + username.toLowerCase());
1257
+ ok = (md5Hash === storedHash);
1258
+ }
1259
+ if (!ok) {
1260
+ // La contraseña antigua es incorrecta
1261
+ return false;
1262
+ }
1263
+ } else {
1264
+ // Si no se requiere la contraseña antigua continua
1265
+ ok = true;
1108
1266
  }
1109
- var result = await client.query(sql, params).fetchOneRowIfExists();
1110
- var ok = result.rowCount == 1;
1111
- if(ok && be.config.login['double-dragon']){
1112
- sql = "ALTER USER " +be.db.quoteIdent(username) + " WITH PASSWORD " + be.db.quoteLiteral(newPassword);
1113
- await client.query(sql,[]).execute();
1267
+ // Si la verificación fue exitosa o se omitió (ok == true), generamos el nuevo hash SCRAM
1268
+ if (ok) {
1269
+ // hash en ormato completo de PostgreSQL: SCRAM-SHA-256$i:salt$verifier
1270
+ const newScramVerifier = await generateScramVerifier(newPassword);
1271
+ const updateSql =
1272
+ "UPDATE " + (schema ? be.db.quoteIdent(schema) + '.' : '') +
1273
+ be.db.quoteIdent(table) +
1274
+ "\n SET " + be.db.quoteIdent(passFieldName) + " = $2 " +
1275
+ "\n WHERE " + be.db.quoteIdent(userFieldName) + " = $1 " +
1276
+ "\n RETURNING 1 as ok";
1277
+
1278
+ const updateParams = [username, newScramVerifier];
1279
+ const updateResult = await client.query(updateSql, updateParams).fetchOneRowIfExists();
1280
+ ok = updateResult.rowCount === 1;
1281
+ if (ok && be.config.login['double-dragon']) {
1282
+ const doubleDragonSql = "ALTER USER " + be.db.quoteIdent(username) + " WITH PASSWORD " + be.db.quoteLiteral(newPassword);
1283
+ await client.query(doubleDragonSql, []).execute();
1284
+ }
1114
1285
  }
1115
1286
  return ok;
1116
- }
1287
+ };
1117
1288
  be.passwordChanger=function(req, username, oldPassword, newPassword, done) {
1118
1289
  if(be.config.login.disableChangePassword){
1119
1290
  done(null,false,'el cambio de contraseña está deshabilitado');
@@ -3384,8 +3555,10 @@ AppBackend.prototype.exportacionesGenerico = async function exportacionesGeneric
3384
3555
  }
3385
3556
  }
3386
3557
  }
3387
- await buscarGenerados(csvFileName);
3388
- await buscarGenerados(fileName);
3558
+ if (!procedureDef.forExport.generarInmediato) {
3559
+ await buscarGenerados(csvFileName);
3560
+ await buscarGenerados(fileName);
3561
+ }
3389
3562
  if(hayGenerados.length>0){
3390
3563
  return hayGenerados;
3391
3564
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "backend-plus",
3
3
  "description": "Backend for the anti Pareto rule",
4
- "version": "2.5.2-betha.23",
4
+ "version": "2.5.2-betha.25",
5
5
  "author": "Codenautas <codenautas@googlegroups.com>",
6
6
  "license": "MIT",
7
7
  "repository": "codenautas/backend-plus",