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.
- package/for-client/chpass.jade +2 -1
- package/for-client/my-menu.js +4 -0
- package/for-client/my-tables.js +15 -5
- package/for-client/my-things.js +1 -0
- package/lib/backend-plus.d.ts +24 -13
- package/lib/backend-plus.js +448 -148
- package/lib/table-def-adapt.js +4 -1
- package/package.json +30 -28
package/for-client/chpass.jade
CHANGED
package/for-client/my-menu.js
CHANGED
|
@@ -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
|
}
|
package/for-client/my-tables.js
CHANGED
|
@@ -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(
|
|
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=(
|
package/for-client/my-things.js
CHANGED
|
@@ -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);
|
package/lib/backend-plus.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
223
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/lib/backend-plus.js
CHANGED
|
@@ -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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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:
|
|
679
|
+
Edge: {short:'Ed' , minVer:14 , polly:117},
|
|
525
680
|
Konqueror: {short:'Kq' , minVer:null, polly:true},
|
|
526
|
-
Chromium: {short:'Chmm', minVer:49 , polly:
|
|
527
|
-
Chrome: {short:'Ch' , minVer:49 , polly:
|
|
528
|
-
Safari: {short:'Sf' , minVer:9 , polly:
|
|
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:
|
|
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
|
-
|
|
647
|
-
var
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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(`
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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(
|
|
935
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
client =
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
var infoFieldList=
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
2669
|
-
|
|
2670
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
3193
|
+
await Promise.all(fieldsForSequences.map(async function(fieldDef) {
|
|
2905
3194
|
var sequence = fieldDef.sequence;
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
"
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
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
|
|
2953
|
-
|
|
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
|
|
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
|
-
|
|
2970
|
-
|
|
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',
|
|
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
|
-
|
|
3342
|
-
|
|
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
|
}
|
package/lib/table-def-adapt.js
CHANGED
|
@@ -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
|
+
"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.
|
|
35
|
-
"best-globals": "^2.0.
|
|
34
|
+
"backend-skins": "^0.1.17",
|
|
35
|
+
"best-globals": "^2.0.2",
|
|
36
36
|
"big.js": "^7.0.1",
|
|
37
|
-
"body-parser": "^2.2.
|
|
38
|
-
"cast-error": "^0.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.
|
|
44
|
-
"discrepances": "^0.2.
|
|
45
|
-
"express": "^5.1
|
|
46
|
-
"express-session": "^1.18.
|
|
47
|
-
"express-useragent": "^
|
|
48
|
-
"fs-extra": "^11.3.
|
|
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.
|
|
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.
|
|
54
|
+
"login-plus": "^1.8.0",
|
|
55
55
|
"memorystore": "^1.6.7",
|
|
56
|
-
"mini-tools": "^1.13.
|
|
56
|
+
"mini-tools": "^1.13.4",
|
|
57
57
|
"moment": "^2.30.1",
|
|
58
58
|
"multiparty": "^4.2.3",
|
|
59
|
-
"nodemailer": "^7.0.
|
|
59
|
+
"nodemailer": "^7.0.12",
|
|
60
60
|
"numeral": "^2.0.6",
|
|
61
|
-
"pg-promise-strict": "^1.4.
|
|
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.
|
|
67
|
+
"require-bro": "^0.3.2",
|
|
67
68
|
"self-explain": "^0.11.0",
|
|
68
|
-
"serve-content": "^0.
|
|
69
|
+
"serve-content": "^1.0.2",
|
|
69
70
|
"session-file-store": "^1.5.0",
|
|
70
|
-
"
|
|
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.
|
|
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": "^
|
|
87
|
-
"@types/nodemailer": "^
|
|
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.
|
|
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
|
|
103
|
+
"mocha": "^11.7.5",
|
|
102
104
|
"nyc": "^17.1.0",
|
|
103
|
-
"puppeteer": "^24.
|
|
104
|
-
"sinon": "^
|
|
105
|
-
"supertest": "^7.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.
|
|
109
|
+
"typescript": "^5.9.3",
|
|
108
110
|
"why-is-node-running": "^3.2.2"
|
|
109
111
|
},
|
|
110
112
|
"engines": {
|