backend-plus 2.5.2-betha.4 → 2.5.2-betha.41

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.
@@ -59,7 +59,8 @@ html(lang=lang)
59
59
  td#resultDiff
60
60
  tr
61
61
  td
62
- td
62
+ td.actions
63
+ a(href="./menu")=msgs.chpass.cancel
63
64
  input(type="submit", value=msgs.chpass.button)
64
65
  each errorLine in (flash.error||[])
65
66
  tr(style={color:'red'})
@@ -199,6 +199,7 @@ myOwn.wScreens.proc.result={
199
199
  }else{
200
200
  divResult.textContent = result;
201
201
  divResult.style.backgroundColor = '#9FA';
202
+ divResult.setAttribute('result','success');
202
203
  }
203
204
  },
204
205
  showGrid:function(result, divResult){
@@ -207,6 +208,8 @@ myOwn.wScreens.proc.result={
207
208
  showError:function(err, divResult){
208
209
  divResult.textContent = err.message;
209
210
  divResult.style.backgroundColor = 'orange';
211
+ divResult.setAttribute('result','error');
212
+
210
213
  },
211
214
  showDownloadUrl:function(result, divResult){
212
215
  divResult.innerHTML='';
@@ -483,6 +486,7 @@ myOwn.displayMainMenu = function(addrParams){
483
486
  if(mtv){
484
487
  if(location.href.match(/^.{0,8}localhost/)){
485
488
  mtv.style.backgroundImage='repeating-linear-gradient(-45deg, #FF8, #FF8 8px, #DDD 8px, #DDD 16px)'
489
+ mtv.setAttribute('dev-mode',true);
486
490
  mtv.title = 'Warning! Using system in localhost!'
487
491
  }
488
492
  }
@@ -903,7 +903,7 @@ myOwn.ActionColumnGrid.prototype.td = function td(depot){
903
903
  actionNamesList.forEach(function(actionName){
904
904
  var actionDef = my.tableAction[actionName];
905
905
  if(grid.def.allow[actionName] && depot.allow[actionName] !== false){
906
- var buttonAction=html.button({class:'table-button', "skip-enter":true}, [
906
+ var buttonAction=html.button({class:'table-button', "skip-enter":true, "bp-action":actionDef.alt}, [
907
907
  html.img({src:actionDef.img, alt:actionDef.alt, title:my.messages[actionDef.titleMsg]})
908
908
  ]).create();
909
909
  thActions.appendChild(buttonAction);
@@ -1983,7 +1983,7 @@ myOwn.TableGrid.prototype.prepareGrid = function prepareGrid(){
1983
1983
  });
1984
1984
  }
1985
1985
  if(grid.def.allow.insert && !grid.def.forInsertOnlyMode){
1986
- buttonInsert=html.button({class:'table-button', "enter-clicks":true}, [
1986
+ buttonInsert=html.button({class:'table-button', "enter-clicks":true, "bp-action": "INS"}, [
1987
1987
  html.img({
1988
1988
  src:my.path.img+'insert.png',
1989
1989
  alt:'INS',
@@ -2203,6 +2203,9 @@ myOwn.specialDefaultValue={
2203
2203
  return belowDepot.row[fieldName]?belowDepot.row[fieldName]+1:(
2204
2204
  aboveDepot.row[fieldName]?aboveDepot.row[fieldName]-1:1
2205
2205
  );
2206
+ },
2207
+ current_user: function specialDefaultValueCurrentUser(){
2208
+ return my.config.username;
2206
2209
  }
2207
2210
  }
2208
2211
 
@@ -2309,7 +2312,7 @@ myOwn.TableGrid.prototype.displayGrid = function displayGrid(){
2309
2312
  grid.updateRowData = function updateRowData(depot, skipUpdateStatus){
2310
2313
  var grid = this;
2311
2314
  var forInsert = false; // not define how to detect
2312
- var tr = depot;
2315
+ var tr = depot.tr;
2313
2316
  grid.setRowStyle(depot,depot.row, skipUpdateStatus);
2314
2317
  grid.def.fields.filter(function(fieldDef){
2315
2318
  return fieldDef.visible;
@@ -2341,6 +2344,12 @@ myOwn.TableGrid.prototype.displayGrid = function displayGrid(){
2341
2344
  depot.clientSidePrepared=true;
2342
2345
  grid.my.clientSides[grid.def.clientSide].update(depot);
2343
2346
  }
2347
+ if (depot.allow.update) {
2348
+ tr.setAttribute('can-update','yes')
2349
+ }
2350
+ if (depot.allow.delete) {
2351
+ tr.setAttribute('can-delete','yes')
2352
+ }
2344
2353
  };
2345
2354
  var saveRow = function(depot, opts){
2346
2355
  if(!('saving' in depot)){
@@ -2490,7 +2499,7 @@ myOwn.TableGrid.prototype.displayGrid = function displayGrid(){
2490
2499
  }
2491
2500
  }
2492
2501
  }
2493
- }else if(!/^\$allow\./.test(fieldName)){
2502
+ }else if(/^\$allow\./.test(fieldName)){
2494
2503
  depot.allow[fieldName.replace(/^\$allow\./,'')] = retrievedRow[fieldName];
2495
2504
  }
2496
2505
  }
@@ -2708,7 +2717,7 @@ myOwn.TableGrid.prototype.displayGrid = function displayGrid(){
2708
2717
  if(iRow<depotsToDisplay.length){
2709
2718
  var addButtonRest = function addButtonRest(toNextRowNumber){
2710
2719
  var createRestButtonInto = function(domElement){
2711
- var buttonRest=html.button({class:'foot-info', "enter-clicks":true},"+..."+toNextRowNumber).create();
2720
+ var buttonRest=html.button({class:'foot-info', "enter-clicks":true, "all-rows-displayed": "no"},"+..."+toNextRowNumber).create();
2712
2721
  domElement.appendChild(html.span(' ').create());
2713
2722
  domElement.appendChild(buttonRest);
2714
2723
  return buttonRest;
@@ -2738,6 +2747,7 @@ myOwn.TableGrid.prototype.displayGrid = function displayGrid(){
2738
2747
  if(grid.dom.buttonInsert){
2739
2748
  grid.dom.buttonInsert.style.visibility='visible';
2740
2749
  }
2750
+ grid.dom.headInfo.rowCount.setAttribute("all-rows-displayed", "yes")
2741
2751
  }
2742
2752
  };
2743
2753
  var linesToDisplay=(
@@ -322,6 +322,7 @@ myOwn.insertRow = function insertRow(where){
322
322
  where.smooth={};
323
323
  }
324
324
  var trDummy = section.insertRow(iRow);
325
+ trDummy.setAttribute('dummy','inserting')
325
326
  tr.style.display='none';
326
327
  for(var i=0; i<(where.smooth.colCount||1); i++){
327
328
  var cell=trDummy.insertCell(-1);
@@ -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
 
@@ -172,10 +173,14 @@ export type SequenceDefinition = {
172
173
  name:string
173
174
  firstValue:number
174
175
  prefix?:string /* Prefix for the generated value */
176
+ }
177
+ export type SequenceMadMaxDefinition = {
178
+ madMax: string[] // grouping of mad max sequences
179
+ firstValue?: number
175
180
  }
176
181
  export type ExportMetadataDefinition={ /* TODO: define */ }
177
182
  export type PostInputOptions='upperSpanish' | 'upperWithoutDiacritics' | 'parseDecimal'
178
- export type FieldDefinition = EditableDbDefinition & {
183
+ export interface FieldDefinition extends EditableDbDefinition {
179
184
  name:string
180
185
  typeName:PgKnownTypes|'ARRAY:text'
181
186
  label?:string
@@ -184,8 +189,9 @@ export type FieldDefinition = EditableDbDefinition & {
184
189
  dbNullable?:boolean /* dbNullable === false is not nullabla at DB level, but not at CLIENT LEVEL */
185
190
  defaultValue?:any
186
191
  defaultDbValue?:PgKnownDbValues|string
192
+ specialDefaultValue?:string /* keyof myOwn.specialDefaultValues and/or keyof AppBackend.prototype.specialSqlDefaultExpressions */
187
193
  clientSide?:string /* keyof: myOwn.clientSides */
188
- isName?:boolean
194
+ isName?:boolean|'known' /* is a name but it is a well known name (because the user uses it to thier code or because the code is enugh expresive)
189
195
  isPk?:number /* internal: pos in the primaryKey array */
190
196
  serverSide?:boolean /* default:!clientSide if the value is retrived from the database */
191
197
  inTable?:boolean /* default:!clientSide && !sql.fields[...].expr. Is a real fisical field in the table */
@@ -200,7 +206,6 @@ export type FieldDefinition = EditableDbDefinition & {
200
206
  references?:string /* table name */
201
207
  referencesField?:string
202
208
  aggregate?:'avg'|'sum'|'count'|'min'|'max'|'countTrue' /* keyof myOwn.TableAggregates */
203
- specialDefaultValue?:string /* keyof myOwn.specialDefaultValues
204
209
  defaultForOtherFields?:boolean /* the field that stores the "other fields" of a flexible imported table */
205
210
  specialValueWhenInsert?:string
206
211
  exportMetadata?:ExportMetadataDefinition
@@ -218,12 +223,9 @@ export type FieldDefinition = EditableDbDefinition & {
218
223
  alwaysShow?:boolean /* show when appears in fixed fields */
219
224
  suggestingKeys?:string[]
220
225
  postInput?:PostInputOptions
221
- } & ({} | {
222
- sequence:SequenceDefinition
223
- nullable:true
224
- editable:false
225
- })
226
- export type EditableDbDefinition = {
226
+ sequence?: SequenceDefinition|SequenceMadMaxDefinition
227
+ }
228
+ export interface EditableDbDefinition {
227
229
  editable?:boolean
228
230
  allow?:{
229
231
  update?:boolean
@@ -240,13 +242,15 @@ export type FieldsForConnect = (string | {source:string, target:string})[]
240
242
  export type FieldsForConnectDetailTable = (string | {source:string, target:string, nullMeansAll?:boolean} | {value:any, target:string})[]
241
243
 
242
244
  export type FkActions = 'no action'|'restrict'|'cascade'|'set null'|'set default';
243
- export type ForeignKey = {
245
+ export interface ForeignKey {
244
246
  references:string,
245
247
  fields:FieldsForConnect,
246
248
  onUpdate?: FkActions,
247
249
  onDelete?: FkActions,
248
250
  displayAllFields?: boolean,
249
- alias?:string,
251
+ alias?:string,
252
+ abr?:string,
253
+ label?:string,
250
254
  displayFields?:string[],
251
255
  consName?:string,
252
256
  initiallyDeferred?:boolean
@@ -395,7 +399,9 @@ export interface AppConfigServer
395
399
  "kill-9": string // a way to kill from URL with a token
396
400
  bitacoraSchema: string
397
401
  bitacoraTableName: string
398
- allowedHosts:string[] //API allowed hosts
402
+ useCors: boolean //habilita Cross-Origin Resource Sharing
403
+ allowedHosts:string[] //determina API allowed hosts (necesita habilitar useCors)
404
+ policy?:string
399
405
  }
400
406
  export interface AppConfigDb
401
407
  {
@@ -432,6 +438,9 @@ export interface AppConfigLogin
432
438
  store:{
433
439
  module: string
434
440
  }
441
+ successRedirect: string // where to redirect after login
442
+ failureRedirect: string // where to redirect after login failure
443
+ failedLoginUrlPath: string // where to redirect after a failed login
435
444
  }
436
445
  forget: { // forget password configurations:
437
446
  urlPath: string // url sent by mail. default: `/new-pass`
@@ -501,6 +510,7 @@ export interface AppConfig {
501
510
  results: boolean // if query results must be included in full db logs
502
511
  }
503
512
  session: boolean // if all session activity must be logged
513
+ "pass-migration"?: boolean
504
514
  }
505
515
  devel: {
506
516
  delay: number // msec avg random delay in API responses (to emulate slow nets)
@@ -544,6 +554,7 @@ export class AppBackend{
544
554
  dbUserNameExpr:string
545
555
  dbUserRolExpr:string
546
556
  specialValueWhenInsert:{[k:string]:(context:ProcedureContext, defField:FieldDefinition, parameters:object)=>any}
557
+ specialSqlDefaultExpressions:Record<string, string>
547
558
  clearCaches():void
548
559
  start(opts?: StartOptions):Promise<void>
549
560
  getTables():TableItemDef[]
@@ -582,7 +593,7 @@ export class AppBackend{
582
593
  messages:Record<LangId,Record<string, string>>
583
594
  }
584
595
  shutdownCallbackListAdd(param:{message:string, fun:()=>Promise<void>}):void
585
- shutdownBackend():Promise<void>
596
+ shutdownBackend(opts?:{skipTurnOff?:boolean, onlyTurnOff?:boolean}):Promise<void>
586
597
  setLog(opts:{until:string, results?:boolean}):void
587
598
  getDataDumpTransformations(rawData:string):Promise<{rawData:string, prepareTransformationSql:string[], endTransformationSql:string[]}>
588
599
  }
@@ -20,6 +20,7 @@ MiniTools.globalOpts.serveErr.propertiesWhiteList=['message','detail','code','ta
20
20
  var crypto = require('crypto');
21
21
  var serveContent = require('serve-content');
22
22
  var pg = require('pg-promise-strict');
23
+ var pgTriggers = require('pg-triggers');
23
24
  var SessionFileStore = require('session-file-store');
24
25
  var memorystore = require('memorystore');
25
26
  var jsToHtml=require('js-to-html');
@@ -82,7 +83,159 @@ function md5(text){
82
83
  return crypto.createHash('md5').update(text).digest('hex');
83
84
  }
84
85
 
85
- //
86
+ const DEFAULT_ITERATIONS = 4096;
87
+ const HASH_ALGORITHM = 'sha256';
88
+
89
+ // primera version de scram sha 256. El Verifier completo tenía 64 bytes.
90
+ const LEGACY_KEY_LENGTH = 64;
91
+
92
+ // Formato nuevo/PG-Compatible: El Client Key tiene 32 bytes (SHA-256).
93
+ const SCRAM_KEY_LENGTH = 32;
94
+
95
+ const bufferToBase64 = (buffer) => buffer.toString('base64');
96
+
97
+ /**
98
+ * Función central para PBKDF2 (Acepta KEY_LEN para compatibilidad).
99
+ * Devuelve la clave derivada con la longitud especificada.
100
+ */
101
+ async function deriveKey(password, saltBuffer, iterations, keyLength) {
102
+ return new Promise((resolve, reject) => {
103
+ crypto.pbkdf2(
104
+ password,
105
+ saltBuffer,
106
+ iterations,
107
+ keyLength,
108
+ HASH_ALGORITHM,
109
+ (err, derivedKey) => {
110
+ if (err) return reject(err);
111
+ resolve(derivedKey);
112
+ }
113
+ );
114
+ });
115
+ }
116
+
117
+ async function generateScramVerifier(password) {
118
+ const saltBuffer = crypto.randomBytes(16);
119
+ const saltBase64 = bufferToBase64(saltBuffer);
120
+
121
+ const Hi = await deriveKey(
122
+ password,
123
+ saltBuffer,
124
+ DEFAULT_ITERATIONS,
125
+ SCRAM_KEY_LENGTH
126
+ );
127
+ const clientKey = crypto.createHmac('sha256', Hi).update('Client Key').digest();
128
+ const serverKey = crypto.createHmac('sha256', Hi).update('Server Key').digest();
129
+
130
+ const storedKey = crypto.createHash('sha256').update(clientKey).digest();
131
+
132
+ const storedKeyBase64 = bufferToBase64(storedKey);
133
+ const serverKeyBase64 = bufferToBase64(serverKey);
134
+
135
+ return `SCRAM-SHA-256$${DEFAULT_ITERATIONS}:${saltBase64}$${storedKeyBase64}:${serverKeyBase64}`;
136
+ }
137
+
138
+ /**
139
+ * Verifica una contraseña contra el hash SCRAM PG-Compatible.
140
+ * Formato PG: SCRAM-SHA-256$<iteraciones>:<Salt>$<StoredKey>:<ServerKey>
141
+ * @param {string} password - Contraseña en texto plano.
142
+ * @param {string} storedScramString - Cadena SCRAM de PostgreSQL.
143
+ * @returns {Promise<boolean>} True si la contraseña es válida.
144
+ */
145
+ async function verifyScramPG(password, storedScramString) {
146
+ if (!storedScramString.startsWith('SCRAM-SHA-256$')) {
147
+ return false;
148
+ }
149
+
150
+ const parts = storedScramString.split('$');
151
+ if (parts.length !== 3) {
152
+ return false;
153
+ }
154
+
155
+ const [storedIterations, saltBase64] = parts[1].split(':');
156
+ const [storedKeyBase64, serverKeyBase64] = parts[2].split(':'); // Asume StoredKey y ServerKey
157
+
158
+ if (!saltBase64 || !storedKeyBase64 || !serverKeyBase64 || isNaN(parseInt(storedIterations))) {
159
+ return false;
160
+ }
161
+
162
+ const iterations = parseInt(storedIterations);
163
+ const saltBuffer = Buffer.from(saltBase64, 'base64');
164
+ const storedKeyBuffer = Buffer.from(storedKeyBase64, 'base64');
165
+ const serverKeyBuffer = Buffer.from(serverKeyBase64, 'base64');
166
+
167
+ const Hi = await deriveKey(
168
+ password,
169
+ saltBuffer,
170
+ iterations,
171
+ SCRAM_KEY_LENGTH
172
+ );
173
+
174
+ const clientKey = crypto.createHmac(HASH_ALGORITHM, Hi)
175
+ .update('Client Key')
176
+ .digest();
177
+
178
+ const serverKeyActual = crypto.createHmac(HASH_ALGORITHM, Hi)
179
+ .update('Server Key')
180
+ .digest();
181
+
182
+ const storedKeyActual = crypto.createHash('sha256').update(clientKey).digest();
183
+
184
+ const storedKeyMatch = crypto.timingSafeEqual(storedKeyActual, storedKeyBuffer);
185
+ const serverKeyMatch = crypto.timingSafeEqual(serverKeyActual, serverKeyBuffer);
186
+ return storedKeyMatch && serverKeyMatch;
187
+ }
188
+
189
+ /**
190
+ * Verifica una contraseña contra el hash SCRAM Legacy (64 bytes).
191
+ * Formato Legacy: SCRAM-SHA-256$<iteraciones>:<Salt>$<Verifier(64 bytes)>
192
+ * @param {string} password - Contraseña en texto plano.
193
+ * @param {string} storedScramString - Cadena SCRAM Legacy.
194
+ * @returns {Promise<boolean>} True si la contraseña es válida.
195
+ */
196
+ async function verifyScramLegacy(password, storedScramString) {
197
+ if (!storedScramString.startsWith('SCRAM-SHA-256$')) {
198
+ // No es formato SCRAM. Debe ser MD5 u otro algoritmo.
199
+ return false;
200
+ }
201
+
202
+ // Parsea la cadena SCRAM
203
+ // Ejemplo: SCRAM-SHA-256$4096:SALT_BASE64$VERIFIER_BASE64
204
+ const parts = storedScramString.split('$');
205
+ if (parts.length !== 3) {
206
+ throw new Error('Formato SCRAM almacenado inválido.');
207
+ }
208
+
209
+ // Obtiene iteraciones y salt de la segunda parte (ej: '4096:SALT_BASE64')
210
+ const [storedIterations, storedSalt] = parts[1].split(':');
211
+ const storedVerifier = parts[2];
212
+
213
+ if (!storedSalt || !storedVerifier || isNaN(parseInt(storedIterations))) {
214
+ throw new Error('Datos de SCRAM incompletos o malformados.');
215
+ }
216
+
217
+ const iterations = parseInt(storedIterations);
218
+
219
+ // Deriva la clave de la contraseña ingresada
220
+ const generatedVerifierBuffer = await deriveKey(
221
+ password,
222
+ storedSalt,
223
+ iterations,
224
+ LEGACY_KEY_LENGTH
225
+ );
226
+ const generatedVerifier = bufferToBase64(generatedVerifierBuffer);
227
+
228
+ // Compara de manera segura contra el Verificador Almacenado
229
+ const storedVerifierBuffer = Buffer.from(storedVerifier, 'base64');
230
+ const generatedVerifierBufferFromBase64 = Buffer.from(generatedVerifier, 'base64');
231
+
232
+ // Usa crypto.timingSafeEqual para evitar ataques de temporización
233
+ if (generatedVerifierBufferFromBase64.length !== storedVerifierBuffer.length) {
234
+ return false;
235
+ }
236
+
237
+ return crypto.timingSafeEqual(generatedVerifierBufferFromBase64, storedVerifierBuffer);
238
+ }
86
239
 
87
240
  var dist=regexpDistCheck.test(packagejson.main)?'dist/':'';
88
241
 
@@ -235,12 +388,13 @@ AppBackend.prototype.i18n.messages.en={
235
388
  button: "Sign In"
236
389
  },
237
390
  chpass:{
238
- title: 'change password',
239
- username: 'Username',
240
- oldPassword:'Old password',
241
- newPassword:'New password',
242
- repPassword:'Reenter password',
243
- button:'Change',
391
+ title: 'change password',
392
+ username: 'Username',
393
+ oldPassword:'Old password',
394
+ newPassword:'New password',
395
+ repPassword:'Reenter password',
396
+ button:'Change',
397
+ cancel: 'Cancel'
244
398
  },
245
399
  fileUploaded:'file uploaded',
246
400
  unkownExt:'unkown file extension'
@@ -315,12 +469,13 @@ AppBackend.prototype.i18n.messages.es={
315
469
  },
316
470
  logged:{
317
471
  chpass:{
318
- title: 'cambio de clave',
319
- username: 'usuario',
320
- oldPassword:'clave anterior',
321
- newPassword:'nueva clave',
322
- repPassword:'repetir nueva clave',
323
- button:'Cambiar',
472
+ title: 'cambio de clave',
473
+ username: 'usuario',
474
+ oldPassword:'clave anterior',
475
+ newPassword:'nueva clave',
476
+ repPassword:'repetir nueva clave',
477
+ button:'Cambiar',
478
+ cancel: 'Cancelar'
324
479
  },
325
480
  fileUploaded:'archivo subido',
326
481
  unkownExt:'extensión de archivo desconocida'
@@ -521,14 +676,14 @@ AppBackend.prototype.shutdownCallbackListAdd = function(messageFun){
521
676
 
522
677
 
523
678
  AppBackend.prototype._Browsers = {
524
- Edge: {short:'Ed' , minVer:14 , polly:true},
679
+ Edge: {short:'Ed' , minVer:14 , polly:117},
525
680
  Konqueror: {short:'Kq' , minVer:null, polly:true},
526
- Chromium: {short:'Chmm', minVer:49 , polly:58 },
527
- Chrome: {short:'Ch' , minVer:49 , polly:58 },
528
- Safari: {short:'Sf' , minVer:9 , polly:9 },
681
+ Chromium: {short:'Chmm', minVer:49 , polly:117},
682
+ Chrome: {short:'Ch' , minVer:49 , polly:117},
683
+ Safari: {short:'Sf' , minVer:9 , polly:17},
529
684
  IE: {short:'IE' , minVer:11 , polly:true},
530
685
  Opera: {short:'Op' , minVer:null, polly:true},
531
- Firefox: {short:'FF' , minVer:52 , polly:52 },
686
+ Firefox: {short:'FF' , minVer:52 , polly:119},
532
687
  };
533
688
 
534
689
  var fontExts = [ 'jfproj', 'ttf', 'pfa', 'woff', 'woff2', 'fnt', 'fot', 'otf', 'odttf', 'fon']
@@ -642,33 +797,42 @@ AppBackend.prototype.start = function start(opts){
642
797
  // @ts-ignore : only for testing */
643
798
  this.getMainApp = function getMainApp(){ return mainApp; };
644
799
  }
645
- this.shutdownBackend = async function shutdownBackend(){
646
- console.log('shooting down:');
647
- var waitFor = [
648
- new Promise(function(resolve,reject){
649
- console.log('*','express server');
650
- be.server.close(/** @param {Error} err */function(err){
651
- if(err){
652
- console.log('*', err)
653
- reject(err);
654
- }else{
655
- console.log('*','express server done!')
656
- resolve();
657
- }
658
- });
659
- }),
660
- ...(be.shutdownCallbackList.map(x => (async function(){
661
- console.log('shut:', x.message)
662
- await x.fun()
663
- console.log('done:', x.message)
664
- })()))
665
- ];
666
- console.log('*', 'waiting for all')
667
- await Promise.all(waitFor);
668
- console.log('*', 'all done')
669
- mainApp = null;
670
- console.log('* logWhy',logWhy)
671
- logWhy && logWhy();
800
+ this.shutdownBackend = async function shutdownBackend(opts){
801
+ var shootingDown = opts == null || !opts.onlyTurnOff;
802
+ var turningOff = opts == null || !opts.skipTurnOff;
803
+ if(!shootingDown){
804
+ console.log('shooting down:');
805
+ var waitFor = [
806
+ new Promise(function(resolve,reject){
807
+ console.log('*','express server');
808
+ be.server.close(/** @param {Error} err */function(err){
809
+ if(err){
810
+ console.log('*', err)
811
+ reject(err);
812
+ }else{
813
+ console.log('*','express server done!')
814
+ resolve();
815
+ }
816
+ });
817
+ }),
818
+ ...(be.shutdownCallbackList.map(x => (async function(){
819
+ console.log('shut:', x.message)
820
+ await x.fun()
821
+ console.log('done:', x.message)
822
+ })())),
823
+ ];
824
+ console.log('*', 'waiting for all')
825
+ await Promise.all(waitFor);
826
+ console.log('*', 'all done')
827
+ mainApp = null;
828
+ }
829
+ if(turningOff){
830
+ console.log('turning off:');
831
+ await pg.shutdown();
832
+ console.log('pg shutdown done!');
833
+ console.log('* logWhy',logWhy)
834
+ logWhy && logWhy();
835
+ }
672
836
  };
673
837
  return Promise.resolve().then(function(){
674
838
  var configList=be.configList();
@@ -704,6 +868,9 @@ AppBackend.prototype.start = function start(opts){
704
868
  }
705
869
  var storeModule = be.sessionStores[sessionStoreName];
706
870
  be.config.login.plus.store.module = storeModule;
871
+ }else{
872
+ console.error('###################### DEPRECATED #####################')
873
+ console.error('LACK OF server.session-store')
707
874
  }
708
875
  be.config.install.dump.db.owner=coalesce(be.config.db.owner,be.config.install.dump.db.owner,be.config.db.user);
709
876
  be.config.install.dump.db["owner4special-scripts"]=coalesce(be.config.install.dump.db["owner4special-scripts"],be.config.install.dump.db.owner)
@@ -801,7 +968,7 @@ AppBackend.prototype.start = function start(opts){
801
968
  }
802
969
  }).then(function(client){
803
970
  if(username){
804
- return client.query(`SELECT set_app_user(${be.db.quoteLiteral(username)})`).execute().then(function(){
971
+ return client.query(`CALL set_app_user(${be.db.quoteLiteral(username)})`).execute().then(function(){
805
972
  return client;
806
973
  });
807
974
  }else{
@@ -903,18 +1070,23 @@ AppBackend.prototype.start = function start(opts){
903
1070
  }).then(async function(){
904
1071
  mainApp = express();
905
1072
  //mainApp.use(cookieParser());
906
- const whitelist = ['localhost'].concat(be.config.server.allowedHosts||[]); // Agrega aquí los orígenes de tus aplicaciones
907
- const corsOptions = {
908
- origin: function (origin, callback) {
909
- if (whitelist.some((element)=>origin?.includes(element)) || !origin){
910
- callback(null, true);
911
- }else{
912
- callback(new Error('Not allowed by CORS'));
913
- }
914
- },
915
- credentials: true
916
- };
917
- mainApp.use(cors(corsOptions));
1073
+ if(be.config.server.allowedHosts){
1074
+ if(!Array.isArray(be.config.server.allowedHosts)){
1075
+ throw Error('allowedHosts must be an Array')
1076
+ }
1077
+ const whitelist = ['localhost'].concat(be.config.server.allowedHosts);
1078
+ const corsOptions = {
1079
+ origin: function (origin, callback) {
1080
+ if (whitelist.some((element)=>origin?.includes(element)) || !origin){
1081
+ callback(null, true);
1082
+ }else{
1083
+ callback(new Error('Not allowed by CORS'));
1084
+ }
1085
+ },
1086
+ credentials: true
1087
+ };
1088
+ mainApp.use(cors(corsOptions));
1089
+ }
918
1090
  mainApp.use(bodyParser.urlencoded({extended:true, limit: '50mb'}));
919
1091
  mainApp.use(function(req,res,next){
920
1092
  if((req.headers['content-type']||'').match(/^multipart\/form-data/)){
@@ -931,8 +1103,11 @@ AppBackend.prototype.start = function start(opts){
931
1103
  });
932
1104
  if(be.config.log.req){
933
1105
  mainApp.use(function(req,res,next){
934
- if(!/keep-alive.json$/.test(req.originalUrl) || be.config.log.req['keep-alive']){
935
- console.log('REQ',req.method,req.protocol,req.hostname,req.originalUrl,'from:',req.ip);
1106
+ if(
1107
+ (!/keep-alive.json$/.test(req.originalUrl) || be.config?.log?.req?.['keep-alive']) &&
1108
+ (!/(.png|.jpg|.js|.jsx|.css|.html)$/.test(req.originalUrl) || be.config?.log?.req?.['all'])
1109
+ ){
1110
+ console.log('REQ',req.method,req.protocol,req.hostname,req.originalUrl,'from:',req.ip,req.query);
936
1111
  }
937
1112
  next();
938
1113
  });
@@ -1007,41 +1182,110 @@ AppBackend.prototype.start = function start(opts){
1007
1182
  mainApp.loginPlusManager.closeManager();
1008
1183
  }
1009
1184
  });
1010
- be.shutdownCallbackListAdd({
1011
- message:'pg',
1012
- fun:function(){
1013
- pg.shutdown();
1185
+ const updatePassword = async ({client, username, password, setUpdateDate, errorIfNoResult}) => {
1186
+ const {table, passFieldName, userFieldName, passUpdatedAtFieldName, passAlgorithmFieldName, schema} = be.config.login;
1187
+ const hashPass = await generateScramVerifier(password);
1188
+ let params = [username, hashPass];
1189
+ let setters = [`${be.db.quoteIdent(passFieldName)} = $2`];
1190
+ if(passAlgorithmFieldName){
1191
+ setters.push(`${be.db.quoteIdent(passAlgorithmFieldName)} = $3`);
1192
+ params.push('PG-SHA256')
1014
1193
  }
1015
- });
1194
+ if(setUpdateDate && passUpdatedAtFieldName){
1195
+ setters.push(`${be.db.quoteIdent(passUpdatedAtFieldName)} = current_timestamp`)
1196
+ }
1197
+
1198
+ const result = await client.query(`
1199
+ UPDATE ${(schema ? be.db.quoteIdent(schema) + '.' : '') + be.db.quoteIdent(table)}
1200
+ SET ${setters.join(', ')}
1201
+ WHERE ${be.db.quoteIdent(userFieldName)} = $1
1202
+ returning 1 as ok
1203
+ `, params
1204
+ ).fetchOneRowIfExists();
1205
+
1206
+ if(result.rowCount === 0 && errorIfNoResult){
1207
+ throw Error ('no se encontró el usuario')
1208
+ };
1209
+ return result
1210
+ }
1016
1211
  mainApp.loginPlusManager.setValidatorStrategy(
1017
- function(req, username, password, done) {
1018
- var client;
1019
- if(!be.config.login["preserve-case"]){
1020
- username = username.toLowerCase().trim();
1021
- }
1022
- be.getDbClient(req).then(function(cli){
1023
- client = cli;
1024
- return client.query("select set_app_user('!login')").execute();
1025
- }).then(function(){
1026
- var infoFieldList=be.config.login.infoFieldList||(be.config.login.rolFieldName?[be.config.login.userFieldName,be.config.login.rolFieldName]:[be.config.login.userFieldName]);
1027
- return client.query(
1028
- "SELECT "+infoFieldList.map(function(fieldOrPair){ return fieldOrPair.split(' as ').map(function(ident){ return be.db.quoteIdent(ident)}).join(' as '); })+
1212
+ async function(req, username, password, done) {
1213
+ try{
1214
+ var client;
1215
+ if(!be.config.login["preserve-case"]){
1216
+ username = username.toLowerCase().trim();
1217
+ }
1218
+ client = await be.getDbClient(req);
1219
+ await client.query("CALL set_app_user('!login')").execute();
1220
+ const {passFieldName, rolFieldName, userFieldName} = be.config.login;
1221
+ var infoFieldList=(
1222
+ be.config.login.infoFieldList||(
1223
+ rolFieldName?
1224
+ [userFieldName, rolFieldName]
1225
+ :
1226
+ [userFieldName]
1227
+ )
1228
+ ).concat(passFieldName);
1229
+
1230
+ const sql = "SELECT "+infoFieldList.map(function(fieldOrPair){ return fieldOrPair.split(' as ').map(function(ident){ return be.db.quoteIdent(ident)}).join(' as '); })+
1029
1231
  ", "+be.config.login.activeClausule+" as active "+
1030
1232
  ", "+be.config.login.lockedClausule+" as locked "+
1031
1233
  " FROM "+(be.config.login.from ?? (
1032
1234
  (be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':'')+
1033
1235
  be.db.quoteIdent(be.config.login.table)
1034
1236
  ))+
1035
- " WHERE "+be.db.quoteIdent(be.config.login.userFieldName)+" = $1 "+
1036
- " AND "+be.db.quoteIdent(be.config.login.passFieldName)+" = $2 ",
1037
- [username, md5(password+username)]
1038
- ).fetchOneRowIfExists();
1039
- }).then(function(data){
1237
+ " WHERE "+be.db.quoteIdent(be.config.login.userFieldName)+" = $1 ";
1238
+ const data = await client.query(sql,[username]).fetchOneRowIfExists();
1239
+
1240
+ if(data.rowCount != 1){
1241
+ done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1242
+ return
1243
+ }else{
1244
+ let needsMigration = false;
1245
+ const user = data.row;
1246
+ if(!user[passFieldName]){
1247
+ done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1248
+ return
1249
+ }
1250
+ const usaScramSha256 = user[passFieldName].startsWith('SCRAM-SHA-256$')
1251
+ if (usaScramSha256){
1252
+ let isScramValid = false;
1253
+ // 1. Intento con formato PG-Compatible (Nuevo)
1254
+ if(await verifyScramPG(password, user[passFieldName])){
1255
+ isScramValid = true;
1256
+ }
1257
+
1258
+ // 2. Intento con formato Legacy (64 bytes)
1259
+ else if(await verifyScramLegacy(password, user[passFieldName])){
1260
+ isScramValid = true;
1261
+ needsMigration = true;
1262
+ }
1263
+ if(!isScramValid){
1264
+ done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1265
+ return
1266
+ }
1267
+ }else{
1268
+ if (md5(password+username.toLowerCase()) === user[passFieldName]){
1269
+ needsMigration = true;
1270
+ }else{
1271
+ done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1272
+ return
1273
+ }
1274
+ }
1275
+ if(needsMigration){
1276
+ if (be.config.log["pass-migration"]) console.log('Ejecutando migración a SCRAM...');
1277
+ await updatePassword({client, username, password, setUpdateDate:false, errorIfNoResult:true});
1278
+ if (be.config.log["pass-migration"]) console.log('Migración completada.');
1279
+ }
1280
+ }
1281
+ //continua validando
1040
1282
  if(data.rowCount==1){
1041
1283
  if(!data.row.active){
1042
1284
  done(null,false,{message:be.messages.unlogged.login.inactiveFail});
1285
+ return
1043
1286
  }else if(data.row.locked){
1044
1287
  done(null,false,{message:be.messages.unlogged.login.lockedFail});
1288
+ return
1045
1289
  }else{
1046
1290
  if(req.query["return-to"]){
1047
1291
  req.session.loginReturnTo = req.query["return-to"];
@@ -1049,12 +1293,13 @@ AppBackend.prototype.start = function start(opts){
1049
1293
  if(be.config.login["double-dragon"]){
1050
1294
  be.DoubleDragon.dbParams[username] = changing(be.config.db, {user:username, password});
1051
1295
  }
1052
- return data.row;
1053
1296
  }
1054
1297
  }else{
1055
1298
  done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1299
+ return
1056
1300
  }
1057
- }).then(async function(userInfo){
1301
+
1302
+ const userInfo = data.row;
1058
1303
  if (!userInfo) return;
1059
1304
  if (!be.config.login.skipBitacora) {
1060
1305
  var context = be.getContext(req);
@@ -1069,56 +1314,73 @@ AppBackend.prototype.start = function start(opts){
1069
1314
  userInfo.bitacoraId = sessionInfo.value;
1070
1315
  }
1071
1316
  done(null, userInfo);
1072
- }).then(function(){
1073
- client.done();
1074
- }).catch(function(err){
1317
+ }catch(err){
1075
1318
  if(be.config.login["double-dragon"]){
1076
1319
  done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1077
1320
  }
1078
- if(client && typeof client.done === "function"){
1079
- client.done();
1080
- }
1081
- throw err;
1082
- }).catch(function(err){
1083
1321
  console.log('login error',err);
1084
1322
  console.log(err.stack);
1085
1323
  done(new Error('internal login error'));
1086
- });
1324
+ } finally {
1325
+ if(client && typeof client.done === "function"){
1326
+ client.done();
1327
+ }
1328
+ }
1087
1329
  }
1088
1330
  );
1089
1331
  be.changePassword=async function(client,username,oldPassword,newPassword){
1090
- var sql = "UPDATE "+(be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':'')+
1091
- be.db.quoteIdent(be.config.login.table)+
1092
- "\n SET "+be.db.quoteIdent(be.config.login.passFieldName)+" = $2 "+
1093
- "\n WHERE "+be.db.quoteIdent(be.config.login.userFieldName)+" = $1 "+
1094
- (oldPassword!==false?"\n AND "+be.db.quoteIdent(be.config.login.passFieldName)+" = $3 ":"")+
1095
- "\n RETURNING 1 as ok";
1096
- var params = [username, md5(newPassword+username.toLowerCase())]
1097
- if(oldPassword!==false){
1098
- params.push(md5(oldPassword+username.toLowerCase()))
1332
+ const { table, passFieldName, userFieldName, schema } = be.config.login;
1333
+ let ok = false;
1334
+ const data = await client.query(
1335
+ `SELECT *
1336
+ FROM ${(schema ? be.db.quoteIdent(schema) + '.' : '')}${be.db.quoteIdent(table)}
1337
+ WHERE ${be.db.quoteIdent(userFieldName)} = $1`,
1338
+ [username]
1339
+ ).fetchOneRowIfExists();
1340
+ if (data.rowCount !== 1) {
1341
+ // Usuario no encontrado
1342
+ return false;
1343
+ }
1344
+ const user = data.row
1345
+ const storedHash = user[passFieldName];
1346
+ if (oldPassword !== false) {
1347
+ if (storedHash.startsWith('SCRAM-SHA-256$')) {//Intento SCRAM-SHA-256
1348
+ ok = await verifyScramPG(oldPassword, storedHash);
1349
+ if (!ok) {
1350
+ ok = await verifyScramLegacy(oldPassword, storedHash);
1351
+ }
1352
+ } else { //Intento MD5
1353
+ const md5Hash = md5(oldPassword + username.toLowerCase());
1354
+ ok = (md5Hash === storedHash);
1355
+ }
1356
+ if (!ok) {
1357
+ // La contraseña antigua es incorrecta
1358
+ return false;
1359
+ }
1360
+ } else {
1361
+ // Si no se requiere la contraseña antigua continua
1362
+ ok = true;
1099
1363
  }
1100
- var result = await client.query(sql, params).fetchOneRowIfExists();
1101
- var ok = result.rowCount == 1;
1102
- if(ok && be.config.login['double-dragon']){
1103
- sql = "ALTER USER " +be.db.quoteIdent(username) + " WITH PASSWORD " + be.db.quoteLiteral(newPassword);
1104
- await client.query(sql,[]).execute();
1364
+ // Si la verificación fue exitosa o se omitió (ok == true), generamos el nuevo hash SCRAM
1365
+ if (ok) {
1366
+ const updateResult = await updatePassword({client, username, password:newPassword, setUpdateDate:true, errorIfNoResult:false});
1367
+ ok = updateResult.rowCount === 1;
1368
+ if (ok && be.config.login['double-dragon']) {
1369
+ const doubleDragonSql = "ALTER USER " + be.db.quoteIdent(username) + " WITH PASSWORD " + be.db.quoteLiteral(newPassword);
1370
+ await client.query(doubleDragonSql, []).execute();
1371
+ }
1105
1372
  }
1106
1373
  return ok;
1107
- }
1374
+ };
1108
1375
  be.passwordChanger=function(req, username, oldPassword, newPassword, done) {
1109
1376
  if(be.config.login.disableChangePassword){
1110
1377
  done(null,false,'el cambio de contraseña está deshabilitado');
1111
1378
  }else{
1112
- var client;
1113
- be.inTransaction(req,function(cli){
1114
- client = cli;
1379
+ be.inTransaction(req,function(client){
1115
1380
  return be.changePassword(client,username,oldPassword,newPassword)
1116
1381
  }).then(function(ok){
1117
1382
  done(null,ok,ok?null:be.messages.server.oldPassDontMatch);
1118
1383
  }).catch(function(err){
1119
- //if(client && client.done){
1120
- // client.done();
1121
- //}
1122
1384
  console.log('error changing pass',err);
1123
1385
  console.log('stack',err.stack);
1124
1386
  throw err;
@@ -1236,6 +1498,10 @@ AppBackend.prototype.specialValueWhenInsert = {
1236
1498
  }
1237
1499
  }
1238
1500
 
1501
+ AppBackend.prototype.specialSqlDefaultExpressions = {
1502
+ 'current_user': 'get_app_user()',
1503
+ }
1504
+
1239
1505
  AppBackend.prototype.checkDatabaseStructure = async function checkDatabaseStructure(client){
1240
1506
  var be=this;
1241
1507
  var result = await client.query(`select setting from pg_settings where name='server_version';`).fetchUniqueValue();
@@ -1304,17 +1570,21 @@ AppBackend.prototype.checkDatabaseStructure = async function checkDatabaseStruct
1304
1570
  `;
1305
1571
  throw new Error(message);
1306
1572
  }
1307
- var {rows: sql_routines} = await client.query(`SELECT routine_name, routine_schema, routine_definition
1573
+ var {rows: sql_routines} = await client.query(`SELECT routine_name, routine_schema, routine_type, routine_definition
1308
1574
  FROM information_schema.routines
1309
1575
  WHERE routine_schema in (${be.config.db.search_path.map(path => be.db.quoteLiteral(path)).join(', ')})
1310
1576
  `).fetchAll();
1311
- var sqlRoutines = likeAr.toPlainObject(sql_routines, 'routine_name');
1577
+ var sqlRoutines = likeAr.createIndex(sql_routines, 'routine_name');
1578
+ var set_app_user_RoutineType = sqlRoutines.set_app_user?.routine_type || 'inexsistente';
1579
+ if (set_app_user_RoutineType != 'PROCEDURE') {
1580
+ throw new Error('ERROR en la DB: set_app_user no es un PROCEDURE es ' + set_app_user_RoutineType);
1581
+ }
1312
1582
  var message = ''
1313
1583
  likeAr(AppBackend.prototype.sql_routines).forEach((routine_name, def) => {
1314
1584
  if (sqlRoutines[routine_name] && def.dump.includes(sqlRoutines[routine_name].routine_definition)) {
1315
1585
  message += `
1316
1586
  ----- hay que crear o actualizar la rutina ${routine_name}:
1317
- ${dump}
1587
+ ${def.dump}
1318
1588
  `;
1319
1589
  }
1320
1590
  })
@@ -1886,12 +2156,12 @@ AppBackend.prototype.addUnloggedServices = function addUnloggedServices(mainApp,
1886
2156
  })
1887
2157
  }
1888
2158
  })
2159
+ promise.catch(err=>{
2160
+ console.log('error in /new-password');
2161
+ console.log(err);
2162
+ })
1889
2163
  }
1890
2164
  res.redirect(Path.posix.join(baseUrl, be.config.login.forget?.urlPathOk+'?result'+resultCode));
1891
- promise.catch(err=>{
1892
- console.log('error in /new-password');
1893
- console.log(err);
1894
- })
1895
2165
  })
1896
2166
  mainApp.get(Path.posix.join(baseUrl,'/new-pass'), function(req,res,next){
1897
2167
  var resultCode;
@@ -2422,10 +2692,16 @@ AppBackend.prototype.addLoggedServices = function addLoggedServices(){
2422
2692
  throw err;
2423
2693
  }
2424
2694
  });
2425
- be.app.get('/--version',function(req,res,next){
2695
+ be.app.get('/--version',async function(req,res,next){
2696
+ var simpleGit = require('simple-git');
2697
+ var git = simpleGit(process.cwd());
2426
2698
  var info=[
2427
2699
  html.h1(be.messages.server.versions),
2428
2700
  html.h3([packagejson.name,' ',packagejson.version]),
2701
+ html.ul([
2702
+ html.li([(await git.revparse(['HEAD'])).trim()]),
2703
+ ...((git.tags().all || []).map(tag => html.li([tag]))),
2704
+ ])
2429
2705
  ];
2430
2706
  Promise.resolve().then(function(){
2431
2707
  if(be.isAdmin(req)){
@@ -2653,7 +2929,16 @@ AppBackend.prototype.dumpFkConstraint = function dumpFkConstraint(fk, tableDef,
2653
2929
  return {consName, clause, sourceFieldList};
2654
2930
  }
2655
2931
 
2932
+ AppBackend.prototype.isGeneratedSequence = function isGeneratedSequence(sequence, not){
2933
+ return sequence && ((!sequence.name && !sequence.madMax) == !not)
2934
+ }
2935
+
2936
+ AppBackend.prototype.isSequenceNonGenerated = function isSequenceNonGenerated(sequence){
2937
+ return this.isGeneratedSequence(sequence, true);
2938
+ }
2939
+
2656
2940
  AppBackend.prototype.dumpDbTableFields = function dumpDbTableFields(tableDef, opts = {}, complements = null){
2941
+ var be = this;
2657
2942
  var db = this.db;
2658
2943
  var fields=[];
2659
2944
  tableDef.fields.forEach(function(fieldDef){
@@ -2665,9 +2950,13 @@ AppBackend.prototype.dumpDbTableFields = function dumpDbTableFields(tableDef, op
2665
2950
  fields.push(
2666
2951
  ' '+db.quoteIdent(fieldDef.name)+
2667
2952
  ' '+(fieldDef.dataLength?(fieldType=='text'?'varchar':fieldType)+'('+fieldDef.dataLength+')':fieldType)+
2668
- (fieldDef.defaultValue!=null?' default '+db.quoteLiteral(fieldDef.defaultValue):'')+
2669
- (fieldDef.defaultDbValue!=null?' default '+fieldDef.defaultDbValue:'')+
2670
- (fieldDef.sequence && !fieldDef.sequence.name?' generated always as identity':'')+
2953
+ ( be.specialSqlDefaultExpressions[fieldDef.defaultDbValue] != null ? ' default ' + be.specialSqlDefaultExpressions[fieldDef.defaultDbValue]
2954
+ : fieldDef.defaultDbValue != null ? ' default ' + fieldDef.defaultDbValue
2955
+ : be.specialSqlDefaultExpressions[fieldDef.specialDefaultValue] != null ? ' default ' + be.specialSqlDefaultExpressions[fieldDef.specialDefaultValue]
2956
+ : fieldDef.defaultValue != null ? ' default ' + db.quoteLiteral(fieldDef.defaultValue)
2957
+ : ''
2958
+ ) +
2959
+ (be.isGeneratedSequence(fieldDef.sequence)?' generated always as identity':'')+
2671
2960
  (fieldDef.generatedAs!=null?` generated always as (${fieldDef.generatedAs}) stored`:'')
2672
2961
  );
2673
2962
  if(complements){
@@ -2774,7 +3063,7 @@ AppBackend.prototype.dumpDbSchemaPartial = async function dumpDbSchemaPartial(pa
2774
3063
  lines.push('create table '+cualQuoteTableName+' (');
2775
3064
  var fields = be.dumpDbTableFields(tableDef, opts,
2776
3065
  function complements(fieldDef){
2777
- if(fieldDef.sequence && !fieldDef.sequence.name){
3066
+ if(be.isGeneratedSequence(fieldDef.sequence)){
2778
3067
  tablesWithStrictSequence[tableName]={}
2779
3068
  }
2780
3069
  if(fieldDef.typeName==='text' && !fieldDef.allowEmptyText){
@@ -2798,7 +3087,7 @@ AppBackend.prototype.dumpDbSchemaPartial = async function dumpDbSchemaPartial(pa
2798
3087
  ' alter column '+db.quoteIdent(fieldDef.name)+' set not null;'
2799
3088
  );
2800
3089
  }
2801
- if(fieldDef.sequence && fieldDef.sequence.name){
3090
+ if(be.isSequenceNonGenerated(fieldDef.sequence)){
2802
3091
  fieldsForSequences.push(fieldDef);
2803
3092
  }
2804
3093
  }
@@ -2901,19 +3190,26 @@ AppBackend.prototype.dumpDbSchemaPartial = async function dumpDbSchemaPartial(pa
2901
3190
  });
2902
3191
  lines.push(tableDef.sql.postCreateSqls);
2903
3192
  lines.push('');
2904
- fieldsForSequences.forEach(function(fieldDef) {
3193
+ await Promise.all(fieldsForSequences.map(async function(fieldDef) {
2905
3194
  var sequence = fieldDef.sequence;
2906
- lines.push("CREATE SEQUENCE "+db.quoteIdent(sequence.name)+" START "+db.quoteInteger(sequence.firstValue||1)+";");
2907
- lines.push(
2908
- "ALTER TABLE "+cualQuoteTableName+
2909
- " ALTER COLUMN "+db.quoteIdent(fieldDef.name)+
2910
- (sequence.prefix==null
2911
- ?" SET DEFAULT nextval("+db.quoteLiteral(sequence.name)+"::regclass);"
2912
- :" SET DEFAULT ("+db.quoteLiteral(sequence.prefix)+" || nextval("+db.quoteLiteral(sequence.name)+"::regclass)::text);"
2913
- )
2914
- );
2915
- lines.push('GRANT USAGE, SELECT ON SEQUENCE '+db.quoteIdent(sequence.name)+' TO '+user+';');
2916
- });
3195
+ if (sequence.name) {
3196
+ if (sequence.madMax) throw new Error('a sequence with madMax cannot have name');
3197
+ lines.push("CREATE SEQUENCE "+db.quoteIdent(sequence.name)+" START "+db.quoteInteger(sequence.firstValue||1)+";");
3198
+ lines.push(
3199
+ "ALTER TABLE "+cualQuoteTableName+
3200
+ " ALTER COLUMN "+db.quoteIdent(fieldDef.name)+
3201
+ (sequence.prefix==null
3202
+ ?" SET DEFAULT nextval("+db.quoteLiteral(sequence.name)+"::regclass);"
3203
+ :" SET DEFAULT ("+db.quoteLiteral(sequence.prefix)+" || nextval("+db.quoteLiteral(sequence.name)+"::regclass)::text);"
3204
+ )
3205
+ );
3206
+ lines.push('GRANT USAGE, SELECT ON SEQUENCE '+db.quoteIdent(sequence.name)+' TO '+user+';');
3207
+ } else if (sequence.madMax) {
3208
+ lines.push(await pgTriggers.dumpMaxIdTrigger(tableDef.sql.tableName, fieldDef.name, {grouping: sequence.madMax.grouping, firstValue: sequence.firstValue}));
3209
+ } else {
3210
+ throw new Error('a sequence without madMax nor name');
3211
+ }
3212
+ }));
2917
3213
  lines.push('');
2918
3214
  if(tableDef.sql.policies.enabled){
2919
3215
  policyLines.push(`ALTER TABLE ${cualQuoteTableName} ENABLE ROW LEVEL SECURITY;`);
@@ -2949,8 +3245,8 @@ AppBackend.prototype.dumpDbSchemaPartial = async function dumpDbSchemaPartial(pa
2949
3245
  }
2950
3246
  }
2951
3247
  lines.push(`
2952
- create or replace function set_app_user(p_user text) returns text
2953
- secirotu definer volatile language plpgsql
3248
+ create or replace procedure set_app_user(p_username text)
3249
+ security definer language plpgsql
2954
3250
  as
2955
3251
  $body$
2956
3252
  declare
@@ -2958,7 +3254,7 @@ declare
2958
3254
  ${db.quoteIdent('v_'+fieldName)} text;`
2959
3255
  ).join('')}
2960
3256
  begin
2961
- if p_user = '!login' then
3257
+ if p_username = '!login' then
2962
3258
  ${be.config.login.infoFieldList.map(fieldName => `
2963
3259
  set backend_plus._${fieldName} = '!';`).join('')}
2964
3260
 
@@ -2966,15 +3262,17 @@ begin
2966
3262
  else
2967
3263
  select ${be.config.login.infoFieldList.map(fieldName => db.quoteIdent(fieldName)).join(', ')}
2968
3264
  into ${be.config.login.infoFieldList.map(fieldName => db.quoteIdent('v_'+fieldName)).join(', ')}
2969
- from usuarios u left join personas p using (idper)
2970
- where u."usuario" = p_user;
3265
+
3266
+ from ${(be.config.login.from ?? (
3267
+ (be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':'')+
3268
+ be.db.quoteIdent(be.config.login.table)))}
3269
+ where ${db.quoteIdent(be.config.login.userFieldName)} = p_username;
2971
3270
  ${be.config.login.infoFieldList.map(fieldName => `
2972
3271
  perform set_config('backend_plus._${fieldName}', v_${fieldName}, false);`).join('')}
2973
3272
 
2974
3273
  set backend_plus._mode = normal;
2975
3274
  end if;
2976
- perform set_config('backend_plus._user', p_user, false);
2977
- return p_user;
3275
+ perform set_config('backend_plus._user', p_username, false);
2978
3276
  end;
2979
3277
  $body$;
2980
3278
 
@@ -3338,8 +3636,10 @@ AppBackend.prototype.exportacionesGenerico = async function exportacionesGeneric
3338
3636
  }
3339
3637
  }
3340
3638
  }
3341
- await buscarGenerados(csvFileName);
3342
- await buscarGenerados(fileName);
3639
+ if (!procedureDef.forExport.generarInmediato) {
3640
+ await buscarGenerados(csvFileName);
3641
+ await buscarGenerados(fileName);
3642
+ }
3343
3643
  if(hayGenerados.length>0){
3344
3644
  return hayGenerados;
3345
3645
  }
@@ -137,6 +137,9 @@ function tableDefAdapt(tableDef, context){
137
137
  resultTableDef.field[fieldDef.name]=fieldDef;
138
138
  if(fieldDef.isName){
139
139
  resultTableDef.nameFields.push(fieldDef.name);
140
+ if (fieldDef.isName == 'known' && resultTableDef.hiddenColumns && !resultTableDef.hiddenColumns.includes(fieldDef.name)) {
141
+ resultTableDef.hiddenColumns.push(fieldDef.name);
142
+ }
140
143
  }
141
144
  return fieldDef;
142
145
  });
@@ -174,7 +177,7 @@ function tableDefAdapt(tableDef, context){
174
177
  if(resultTableDef.sortColumns){
175
178
  resultTableDef.sortColumns.map(function(sortColumn){
176
179
  sortColumn.order = sortColumn.order || 1;
177
- if (!resultTableDef.field[sortColumn.column]) {
180
+ if (!resultTableDef.field[sortColumn.column] && !/__/.test(sortColumn.column)) {
178
181
  throw new Error("unkown column " + JSON.stringify(sortColumn.column) + " in sortColumn (or Primarykey) of " + resultTableDef.name);
179
182
  }
180
183
  });
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.4",
4
+ "version": "2.5.2-betha.41",
5
5
  "author": "Codenautas <codenautas@googlegroups.com>",
6
6
  "license": "MIT",
7
7
  "repository": "codenautas/backend-plus",
@@ -31,43 +31,45 @@
31
31
  "dependencies": {
32
32
  "@upgraded/locate-path": "^6.0.0-alfa.1",
33
33
  "ajax-best-promise": "^0.4.2",
34
- "backend-skins": "^0.1.15",
35
- "best-globals": "^2.0.1",
34
+ "backend-skins": "^0.1.17",
35
+ "best-globals": "^2.0.2",
36
36
  "big.js": "^7.0.1",
37
- "body-parser": "^2.2.0",
38
- "cast-error": "^0.1.1",
37
+ "body-parser": "^2.2.1",
38
+ "cast-error": "^0.1.2",
39
39
  "castellano": "^0.1.4",
40
40
  "connect-pg-simple": "^10.0.0",
41
41
  "cookie-parser": "^1.4.7",
42
42
  "cors": "^2.8.5",
43
- "dialog-promise": "^0.10.1",
44
- "discrepances": "^0.2.8",
45
- "express": "^5.1.0",
46
- "express-session": "^1.18.1",
47
- "express-useragent": "^1.0.15",
48
- "fs-extra": "^11.3.0",
43
+ "dialog-promise": "^0.10.2",
44
+ "discrepances": "^0.2.9",
45
+ "express": "^5.2.1",
46
+ "express-session": "^1.18.2",
47
+ "express-useragent": "^2.0.2",
48
+ "fs-extra": "^11.3.3",
49
49
  "js-to-html": "^1.3.2",
50
- "js-yaml": "^4.1.0",
50
+ "js-yaml": "^4.1.1",
51
51
  "json4all": "^1.4.0",
52
52
  "lazy-some": "^0.1.0",
53
53
  "like-ar": "^0.5.1",
54
- "login-plus": "^1.7.2",
54
+ "login-plus": "^1.8.0",
55
55
  "memorystore": "^1.6.7",
56
- "mini-tools": "^1.13.2",
56
+ "mini-tools": "^1.13.4",
57
57
  "moment": "^2.30.1",
58
58
  "multiparty": "^4.2.3",
59
- "nodemailer": "^7.0.3",
59
+ "nodemailer": "^7.0.12",
60
60
  "numeral": "^2.0.6",
61
- "pg-promise-strict": "^1.4.2",
61
+ "pg-promise-strict": "^1.4.3",
62
+ "pg-triggers": "0.4.3",
62
63
  "pikaday": "^1.8.2",
63
64
  "pug": "^3.0.3",
64
65
  "read-yaml-promise": "^1.0.2",
65
66
  "regexplicit": "^0.1.3",
66
- "require-bro": "^0.3.1",
67
+ "require-bro": "^0.3.2",
67
68
  "self-explain": "^0.11.0",
68
- "serve-content": "^0.4.0",
69
+ "serve-content": "^1.0.2",
69
70
  "session-file-store": "^1.5.0",
70
- "sql-tools": "^0.1.2",
71
+ "simple-git": "^3.30.0",
72
+ "sql-tools": "^0.1.4",
71
73
  "stack-trace": "^0.0.10",
72
74
  "stylus": "0.64.0",
73
75
  "type-store": "^0.4.5",
@@ -77,16 +79,16 @@
77
79
  "devDependencies": {
78
80
  "@types/big.js": "^6.2.2",
79
81
  "@types/expect.js": "~0.3.32",
80
- "@types/express": "^5.0.2",
82
+ "@types/express": "^5.0.6",
81
83
  "@types/express-useragent": "^1.0.5",
82
84
  "@types/fs-extra": "^11.0.4",
83
85
  "@types/js-yaml": "^4.0.9",
84
86
  "@types/mocha": "^10.0.10",
85
87
  "@types/multiparty": "~4.2.1",
86
- "@types/node": "^22.15.29",
87
- "@types/nodemailer": "^6.4.17",
88
+ "@types/node": "^25.0.3",
89
+ "@types/nodemailer": "^7.0.4",
88
90
  "@types/numeral": "~2.0.5",
89
- "@types/session-file-store": "^1.2.5",
91
+ "@types/session-file-store": "^1.2.6",
90
92
  "@types/stack-trace": "~0.0.33",
91
93
  "@types/websql": "~0.0.30",
92
94
  "esprima": "^4.0.1",
@@ -98,13 +100,13 @@
98
100
  "karma-ie-launcher": "^1.0.0",
99
101
  "karma-mocha": "^2.0.1",
100
102
  "kill-9": "~0.4.3",
101
- "mocha": "^11.5.0",
103
+ "mocha": "^11.7.5",
102
104
  "nyc": "^17.1.0",
103
- "puppeteer": "^24.9.0",
104
- "sinon": "^20.0.0",
105
- "supertest": "^7.1.1",
105
+ "puppeteer": "^24.34.0",
106
+ "sinon": "^21.0.1",
107
+ "supertest": "^7.1.4",
106
108
  "types.d.ts": "~0.6.22",
107
- "typescript": "^5.8.3",
109
+ "typescript": "^5.9.3",
108
110
  "why-is-node-running": "^3.2.2"
109
111
  },
110
112
  "engines": {