backend-plus 2.5.2-betha.9 → 2.5.2
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 +3 -1
- package/for-client/login.jade +1 -0
- 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 +22 -24
- package/lib/backend-plus.js +412 -132
- package/lib/table-def-adapt.js +4 -1
- package/package.json +39 -37
package/for-client/chpass.jade
CHANGED
|
@@ -9,6 +9,7 @@ html(lang=lang)
|
|
|
9
9
|
.iconostd{
|
|
10
10
|
display: none
|
|
11
11
|
}
|
|
12
|
+
meta(name='viewport' content="width=device-width, user-scalable=no")
|
|
12
13
|
body
|
|
13
14
|
form(id="chPassForm", action="chpass", method="post")
|
|
14
15
|
table.chpasstable
|
|
@@ -59,7 +60,8 @@ html(lang=lang)
|
|
|
59
60
|
td#resultDiff
|
|
60
61
|
tr
|
|
61
62
|
td
|
|
62
|
-
td
|
|
63
|
+
td.actions
|
|
64
|
+
a(href="./menu")=msgs.chpass.cancel
|
|
63
65
|
input(type="submit", value=msgs.chpass.button)
|
|
64
66
|
each errorLine in (flash.error||[])
|
|
65
67
|
tr(style={color:'red'})
|
package/for-client/login.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
|
|
|
@@ -175,11 +176,11 @@ export type SequenceDefinition = {
|
|
|
175
176
|
}
|
|
176
177
|
export type SequenceMadMaxDefinition = {
|
|
177
178
|
madMax: string[] // grouping of mad max sequences
|
|
178
|
-
firstValue
|
|
179
|
+
firstValue?: number
|
|
179
180
|
}
|
|
180
181
|
export type ExportMetadataDefinition={ /* TODO: define */ }
|
|
181
182
|
export type PostInputOptions='upperSpanish' | 'upperWithoutDiacritics' | 'parseDecimal'
|
|
182
|
-
export
|
|
183
|
+
export interface FieldDefinition extends EditableDbDefinition {
|
|
183
184
|
name:string
|
|
184
185
|
typeName:PgKnownTypes|'ARRAY:text'
|
|
185
186
|
label?:string
|
|
@@ -188,8 +189,9 @@ export type FieldDefinition = EditableDbDefinition & {
|
|
|
188
189
|
dbNullable?:boolean /* dbNullable === false is not nullabla at DB level, but not at CLIENT LEVEL */
|
|
189
190
|
defaultValue?:any
|
|
190
191
|
defaultDbValue?:PgKnownDbValues|string
|
|
192
|
+
specialDefaultValue?:string /* keyof myOwn.specialDefaultValues and/or keyof AppBackend.prototype.specialSqlDefaultExpressions */
|
|
191
193
|
clientSide?:string /* keyof: myOwn.clientSides */
|
|
192
|
-
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)
|
|
193
195
|
isPk?:number /* internal: pos in the primaryKey array */
|
|
194
196
|
serverSide?:boolean /* default:!clientSide if the value is retrived from the database */
|
|
195
197
|
inTable?:boolean /* default:!clientSide && !sql.fields[...].expr. Is a real fisical field in the table */
|
|
@@ -204,7 +206,6 @@ export type FieldDefinition = EditableDbDefinition & {
|
|
|
204
206
|
references?:string /* table name */
|
|
205
207
|
referencesField?:string
|
|
206
208
|
aggregate?:'avg'|'sum'|'count'|'min'|'max'|'countTrue' /* keyof myOwn.TableAggregates */
|
|
207
|
-
specialDefaultValue?:string /* keyof myOwn.specialDefaultValues
|
|
208
209
|
defaultForOtherFields?:boolean /* the field that stores the "other fields" of a flexible imported table */
|
|
209
210
|
specialValueWhenInsert?:string
|
|
210
211
|
exportMetadata?:ExportMetadataDefinition
|
|
@@ -222,22 +223,9 @@ export type FieldDefinition = EditableDbDefinition & {
|
|
|
222
223
|
alwaysShow?:boolean /* show when appears in fixed fields */
|
|
223
224
|
suggestingKeys?:string[]
|
|
224
225
|
postInput?:PostInputOptions
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
nullable?: boolean
|
|
229
|
-
editable?: boolean
|
|
230
|
-
} | {
|
|
231
|
-
sequence: SequenceDefinition
|
|
232
|
-
nullable: true
|
|
233
|
-
editable: false
|
|
234
|
-
} | {
|
|
235
|
-
sequence: SequenceMadMaxDefinition
|
|
236
|
-
nullable: true
|
|
237
|
-
editable: boolean
|
|
238
|
-
}
|
|
239
|
-
);
|
|
240
|
-
export type EditableDbDefinition = {
|
|
226
|
+
sequence?: SequenceDefinition|SequenceMadMaxDefinition
|
|
227
|
+
}
|
|
228
|
+
export interface EditableDbDefinition {
|
|
241
229
|
editable?:boolean
|
|
242
230
|
allow?:{
|
|
243
231
|
update?:boolean
|
|
@@ -254,13 +242,15 @@ export type FieldsForConnect = (string | {source:string, target:string})[]
|
|
|
254
242
|
export type FieldsForConnectDetailTable = (string | {source:string, target:string, nullMeansAll?:boolean} | {value:any, target:string})[]
|
|
255
243
|
|
|
256
244
|
export type FkActions = 'no action'|'restrict'|'cascade'|'set null'|'set default';
|
|
257
|
-
export
|
|
245
|
+
export interface ForeignKey {
|
|
258
246
|
references:string,
|
|
259
247
|
fields:FieldsForConnect,
|
|
260
248
|
onUpdate?: FkActions,
|
|
261
249
|
onDelete?: FkActions,
|
|
262
250
|
displayAllFields?: boolean,
|
|
263
|
-
alias?:string,
|
|
251
|
+
alias?:string,
|
|
252
|
+
abr?:string,
|
|
253
|
+
label?:string,
|
|
264
254
|
displayFields?:string[],
|
|
265
255
|
consName?:string,
|
|
266
256
|
initiallyDeferred?:boolean
|
|
@@ -409,7 +399,9 @@ export interface AppConfigServer
|
|
|
409
399
|
"kill-9": string // a way to kill from URL with a token
|
|
410
400
|
bitacoraSchema: string
|
|
411
401
|
bitacoraTableName: string
|
|
412
|
-
|
|
402
|
+
useCors: boolean //habilita Cross-Origin Resource Sharing
|
|
403
|
+
allowedHosts:string[] //determina API allowed hosts (necesita habilitar useCors)
|
|
404
|
+
policy?:string
|
|
413
405
|
}
|
|
414
406
|
export interface AppConfigDb
|
|
415
407
|
{
|
|
@@ -429,6 +421,7 @@ export interface AppConfigLogin
|
|
|
429
421
|
{
|
|
430
422
|
schema: string // schema of the user table
|
|
431
423
|
table: string // user table
|
|
424
|
+
from: string // complete expression to get table or join where get the user
|
|
432
425
|
userFieldname: string // fieldname in user table that stores the user name
|
|
433
426
|
passFieldname: string // fieldname in user table that stores the password hash
|
|
434
427
|
rolFieldname: string // fieldname in user table that stores the rol
|
|
@@ -445,6 +438,9 @@ export interface AppConfigLogin
|
|
|
445
438
|
store:{
|
|
446
439
|
module: string
|
|
447
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
|
|
448
444
|
}
|
|
449
445
|
forget: { // forget password configurations:
|
|
450
446
|
urlPath: string // url sent by mail. default: `/new-pass`
|
|
@@ -514,6 +510,7 @@ export interface AppConfig {
|
|
|
514
510
|
results: boolean // if query results must be included in full db logs
|
|
515
511
|
}
|
|
516
512
|
session: boolean // if all session activity must be logged
|
|
513
|
+
"pass-migration"?: boolean
|
|
517
514
|
}
|
|
518
515
|
devel: {
|
|
519
516
|
delay: number // msec avg random delay in API responses (to emulate slow nets)
|
|
@@ -557,6 +554,7 @@ export class AppBackend{
|
|
|
557
554
|
dbUserNameExpr:string
|
|
558
555
|
dbUserRolExpr:string
|
|
559
556
|
specialValueWhenInsert:{[k:string]:(context:ProcedureContext, defField:FieldDefinition, parameters:object)=>any}
|
|
557
|
+
specialSqlDefaultExpressions:Record<string, string>
|
|
560
558
|
clearCaches():void
|
|
561
559
|
start(opts?: StartOptions):Promise<void>
|
|
562
560
|
getTables():TableItemDef[]
|
|
@@ -595,7 +593,7 @@ export class AppBackend{
|
|
|
595
593
|
messages:Record<LangId,Record<string, string>>
|
|
596
594
|
}
|
|
597
595
|
shutdownCallbackListAdd(param:{message:string, fun:()=>Promise<void>}):void
|
|
598
|
-
shutdownBackend():Promise<void>
|
|
596
|
+
shutdownBackend(opts?:{skipTurnOff?:boolean, onlyTurnOff?:boolean}):Promise<void>
|
|
599
597
|
setLog(opts:{until:string, results?:boolean}):void
|
|
600
598
|
getDataDumpTransformations(rawData:string):Promise<{rawData:string, prepareTransformationSql:string[], endTransformationSql:string[]}>
|
|
601
599
|
}
|
package/lib/backend-plus.js
CHANGED
|
@@ -83,7 +83,159 @@ function md5(text){
|
|
|
83
83
|
return crypto.createHash('md5').update(text).digest('hex');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
const DEFAULT_ITERATIONS = 4096;
|
|
87
|
+
const HASH_ALGORITHM = 'sha256';
|
|
88
|
+
|
|
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
|
+
}
|
|
87
239
|
|
|
88
240
|
var dist=regexpDistCheck.test(packagejson.main)?'dist/':'';
|
|
89
241
|
|
|
@@ -236,12 +388,13 @@ AppBackend.prototype.i18n.messages.en={
|
|
|
236
388
|
button: "Sign In"
|
|
237
389
|
},
|
|
238
390
|
chpass:{
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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'
|
|
245
398
|
},
|
|
246
399
|
fileUploaded:'file uploaded',
|
|
247
400
|
unkownExt:'unkown file extension'
|
|
@@ -316,12 +469,13 @@ AppBackend.prototype.i18n.messages.es={
|
|
|
316
469
|
},
|
|
317
470
|
logged:{
|
|
318
471
|
chpass:{
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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'
|
|
325
479
|
},
|
|
326
480
|
fileUploaded:'archivo subido',
|
|
327
481
|
unkownExt:'extensión de archivo desconocida'
|
|
@@ -522,14 +676,14 @@ AppBackend.prototype.shutdownCallbackListAdd = function(messageFun){
|
|
|
522
676
|
|
|
523
677
|
|
|
524
678
|
AppBackend.prototype._Browsers = {
|
|
525
|
-
Edge: {short:'Ed' , minVer:14 , polly:
|
|
679
|
+
Edge: {short:'Ed' , minVer:14 , polly:117},
|
|
526
680
|
Konqueror: {short:'Kq' , minVer:null, polly:true},
|
|
527
|
-
Chromium: {short:'Chmm', minVer:49 , polly:
|
|
528
|
-
Chrome: {short:'Ch' , minVer:49 , polly:
|
|
529
|
-
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},
|
|
530
684
|
IE: {short:'IE' , minVer:11 , polly:true},
|
|
531
685
|
Opera: {short:'Op' , minVer:null, polly:true},
|
|
532
|
-
Firefox: {short:'FF' , minVer:52 , polly:
|
|
686
|
+
Firefox: {short:'FF' , minVer:52 , polly:119},
|
|
533
687
|
};
|
|
534
688
|
|
|
535
689
|
var fontExts = [ 'jfproj', 'ttf', 'pfa', 'woff', 'woff2', 'fnt', 'fot', 'otf', 'odttf', 'fon']
|
|
@@ -643,33 +797,42 @@ AppBackend.prototype.start = function start(opts){
|
|
|
643
797
|
// @ts-ignore : only for testing */
|
|
644
798
|
this.getMainApp = function getMainApp(){ return mainApp; };
|
|
645
799
|
}
|
|
646
|
-
this.shutdownBackend = async function shutdownBackend(){
|
|
647
|
-
|
|
648
|
-
var
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
+
}
|
|
673
836
|
};
|
|
674
837
|
return Promise.resolve().then(function(){
|
|
675
838
|
var configList=be.configList();
|
|
@@ -705,6 +868,9 @@ AppBackend.prototype.start = function start(opts){
|
|
|
705
868
|
}
|
|
706
869
|
var storeModule = be.sessionStores[sessionStoreName];
|
|
707
870
|
be.config.login.plus.store.module = storeModule;
|
|
871
|
+
}else{
|
|
872
|
+
console.error('###################### DEPRECATED #####################')
|
|
873
|
+
console.error('LACK OF server.session-store')
|
|
708
874
|
}
|
|
709
875
|
be.config.install.dump.db.owner=coalesce(be.config.db.owner,be.config.install.dump.db.owner,be.config.db.user);
|
|
710
876
|
be.config.install.dump.db["owner4special-scripts"]=coalesce(be.config.install.dump.db["owner4special-scripts"],be.config.install.dump.db.owner)
|
|
@@ -802,7 +968,7 @@ AppBackend.prototype.start = function start(opts){
|
|
|
802
968
|
}
|
|
803
969
|
}).then(function(client){
|
|
804
970
|
if(username){
|
|
805
|
-
return client.query(`
|
|
971
|
+
return client.query(`CALL set_app_user(${be.db.quoteLiteral(username)})`).execute().then(function(){
|
|
806
972
|
return client;
|
|
807
973
|
});
|
|
808
974
|
}else{
|
|
@@ -904,18 +1070,23 @@ AppBackend.prototype.start = function start(opts){
|
|
|
904
1070
|
}).then(async function(){
|
|
905
1071
|
mainApp = express();
|
|
906
1072
|
//mainApp.use(cookieParser());
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
+
}
|
|
919
1090
|
mainApp.use(bodyParser.urlencoded({extended:true, limit: '50mb'}));
|
|
920
1091
|
mainApp.use(function(req,res,next){
|
|
921
1092
|
if((req.headers['content-type']||'').match(/^multipart\/form-data/)){
|
|
@@ -932,8 +1103,11 @@ AppBackend.prototype.start = function start(opts){
|
|
|
932
1103
|
});
|
|
933
1104
|
if(be.config.log.req){
|
|
934
1105
|
mainApp.use(function(req,res,next){
|
|
935
|
-
if(
|
|
936
|
-
|
|
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);
|
|
937
1111
|
}
|
|
938
1112
|
next();
|
|
939
1113
|
});
|
|
@@ -1008,41 +1182,110 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1008
1182
|
mainApp.loginPlusManager.closeManager();
|
|
1009
1183
|
}
|
|
1010
1184
|
});
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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')
|
|
1015
1193
|
}
|
|
1016
|
-
|
|
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
|
+
}
|
|
1017
1211
|
mainApp.loginPlusManager.setValidatorStrategy(
|
|
1018
|
-
function(req, username, password, done) {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
client =
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
var infoFieldList=
|
|
1028
|
-
|
|
1029
|
-
|
|
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 '); })+
|
|
1030
1231
|
", "+be.config.login.activeClausule+" as active "+
|
|
1031
1232
|
", "+be.config.login.lockedClausule+" as locked "+
|
|
1032
1233
|
" FROM "+(be.config.login.from ?? (
|
|
1033
1234
|
(be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':'')+
|
|
1034
1235
|
be.db.quoteIdent(be.config.login.table)
|
|
1035
1236
|
))+
|
|
1036
|
-
" WHERE "+be.db.quoteIdent(be.config.login.userFieldName)+" = $1 "
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1041
1282
|
if(data.rowCount==1){
|
|
1042
1283
|
if(!data.row.active){
|
|
1043
1284
|
done(null,false,{message:be.messages.unlogged.login.inactiveFail});
|
|
1285
|
+
return
|
|
1044
1286
|
}else if(data.row.locked){
|
|
1045
1287
|
done(null,false,{message:be.messages.unlogged.login.lockedFail});
|
|
1288
|
+
return
|
|
1046
1289
|
}else{
|
|
1047
1290
|
if(req.query["return-to"]){
|
|
1048
1291
|
req.session.loginReturnTo = req.query["return-to"];
|
|
@@ -1050,12 +1293,13 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1050
1293
|
if(be.config.login["double-dragon"]){
|
|
1051
1294
|
be.DoubleDragon.dbParams[username] = changing(be.config.db, {user:username, password});
|
|
1052
1295
|
}
|
|
1053
|
-
return data.row;
|
|
1054
1296
|
}
|
|
1055
1297
|
}else{
|
|
1056
1298
|
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1299
|
+
return
|
|
1057
1300
|
}
|
|
1058
|
-
|
|
1301
|
+
|
|
1302
|
+
const userInfo = data.row;
|
|
1059
1303
|
if (!userInfo) return;
|
|
1060
1304
|
if (!be.config.login.skipBitacora) {
|
|
1061
1305
|
var context = be.getContext(req);
|
|
@@ -1070,56 +1314,73 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1070
1314
|
userInfo.bitacoraId = sessionInfo.value;
|
|
1071
1315
|
}
|
|
1072
1316
|
done(null, userInfo);
|
|
1073
|
-
}
|
|
1074
|
-
client.done();
|
|
1075
|
-
}).catch(function(err){
|
|
1317
|
+
}catch(err){
|
|
1076
1318
|
if(be.config.login["double-dragon"]){
|
|
1077
1319
|
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1078
1320
|
}
|
|
1079
|
-
if(client && typeof client.done === "function"){
|
|
1080
|
-
client.done();
|
|
1081
|
-
}
|
|
1082
|
-
throw err;
|
|
1083
|
-
}).catch(function(err){
|
|
1084
1321
|
console.log('login error',err);
|
|
1085
1322
|
console.log(err.stack);
|
|
1086
1323
|
done(new Error('internal login error'));
|
|
1087
|
-
}
|
|
1324
|
+
} finally {
|
|
1325
|
+
if(client && typeof client.done === "function"){
|
|
1326
|
+
client.done();
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1088
1329
|
}
|
|
1089
1330
|
);
|
|
1090
1331
|
be.changePassword=async function(client,username,oldPassword,newPassword){
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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;
|
|
1100
1363
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
+
}
|
|
1106
1372
|
}
|
|
1107
1373
|
return ok;
|
|
1108
|
-
}
|
|
1374
|
+
};
|
|
1109
1375
|
be.passwordChanger=function(req, username, oldPassword, newPassword, done) {
|
|
1110
1376
|
if(be.config.login.disableChangePassword){
|
|
1111
1377
|
done(null,false,'el cambio de contraseña está deshabilitado');
|
|
1112
1378
|
}else{
|
|
1113
|
-
|
|
1114
|
-
be.inTransaction(req,function(cli){
|
|
1115
|
-
client = cli;
|
|
1379
|
+
be.inTransaction(req,function(client){
|
|
1116
1380
|
return be.changePassword(client,username,oldPassword,newPassword)
|
|
1117
1381
|
}).then(function(ok){
|
|
1118
1382
|
done(null,ok,ok?null:be.messages.server.oldPassDontMatch);
|
|
1119
1383
|
}).catch(function(err){
|
|
1120
|
-
//if(client && client.done){
|
|
1121
|
-
// client.done();
|
|
1122
|
-
//}
|
|
1123
1384
|
console.log('error changing pass',err);
|
|
1124
1385
|
console.log('stack',err.stack);
|
|
1125
1386
|
throw err;
|
|
@@ -1237,6 +1498,10 @@ AppBackend.prototype.specialValueWhenInsert = {
|
|
|
1237
1498
|
}
|
|
1238
1499
|
}
|
|
1239
1500
|
|
|
1501
|
+
AppBackend.prototype.specialSqlDefaultExpressions = {
|
|
1502
|
+
'current_user': 'get_app_user()',
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1240
1505
|
AppBackend.prototype.checkDatabaseStructure = async function checkDatabaseStructure(client){
|
|
1241
1506
|
var be=this;
|
|
1242
1507
|
var result = await client.query(`select setting from pg_settings where name='server_version';`).fetchUniqueValue();
|
|
@@ -1305,17 +1570,21 @@ AppBackend.prototype.checkDatabaseStructure = async function checkDatabaseStruct
|
|
|
1305
1570
|
`;
|
|
1306
1571
|
throw new Error(message);
|
|
1307
1572
|
}
|
|
1308
|
-
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
|
|
1309
1574
|
FROM information_schema.routines
|
|
1310
1575
|
WHERE routine_schema in (${be.config.db.search_path.map(path => be.db.quoteLiteral(path)).join(', ')})
|
|
1311
1576
|
`).fetchAll();
|
|
1312
|
-
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
|
+
}
|
|
1313
1582
|
var message = ''
|
|
1314
1583
|
likeAr(AppBackend.prototype.sql_routines).forEach((routine_name, def) => {
|
|
1315
1584
|
if (sqlRoutines[routine_name] && def.dump.includes(sqlRoutines[routine_name].routine_definition)) {
|
|
1316
1585
|
message += `
|
|
1317
1586
|
----- hay que crear o actualizar la rutina ${routine_name}:
|
|
1318
|
-
${dump}
|
|
1587
|
+
${def.dump}
|
|
1319
1588
|
`;
|
|
1320
1589
|
}
|
|
1321
1590
|
})
|
|
@@ -1887,12 +2156,12 @@ AppBackend.prototype.addUnloggedServices = function addUnloggedServices(mainApp,
|
|
|
1887
2156
|
})
|
|
1888
2157
|
}
|
|
1889
2158
|
})
|
|
2159
|
+
promise.catch(err=>{
|
|
2160
|
+
console.log('error in /new-password');
|
|
2161
|
+
console.log(err);
|
|
2162
|
+
})
|
|
1890
2163
|
}
|
|
1891
2164
|
res.redirect(Path.posix.join(baseUrl, be.config.login.forget?.urlPathOk+'?result'+resultCode));
|
|
1892
|
-
promise.catch(err=>{
|
|
1893
|
-
console.log('error in /new-password');
|
|
1894
|
-
console.log(err);
|
|
1895
|
-
})
|
|
1896
2165
|
})
|
|
1897
2166
|
mainApp.get(Path.posix.join(baseUrl,'/new-pass'), function(req,res,next){
|
|
1898
2167
|
var resultCode;
|
|
@@ -2423,10 +2692,16 @@ AppBackend.prototype.addLoggedServices = function addLoggedServices(){
|
|
|
2423
2692
|
throw err;
|
|
2424
2693
|
}
|
|
2425
2694
|
});
|
|
2426
|
-
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());
|
|
2427
2698
|
var info=[
|
|
2428
2699
|
html.h1(be.messages.server.versions),
|
|
2429
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
|
+
])
|
|
2430
2705
|
];
|
|
2431
2706
|
Promise.resolve().then(function(){
|
|
2432
2707
|
if(be.isAdmin(req)){
|
|
@@ -2675,8 +2950,12 @@ AppBackend.prototype.dumpDbTableFields = function dumpDbTableFields(tableDef, op
|
|
|
2675
2950
|
fields.push(
|
|
2676
2951
|
' '+db.quoteIdent(fieldDef.name)+
|
|
2677
2952
|
' '+(fieldDef.dataLength?(fieldType=='text'?'varchar':fieldType)+'('+fieldDef.dataLength+')':fieldType)+
|
|
2678
|
-
(fieldDef.
|
|
2679
|
-
|
|
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
|
+
) +
|
|
2680
2959
|
(be.isGeneratedSequence(fieldDef.sequence)?' generated always as identity':'')+
|
|
2681
2960
|
(fieldDef.generatedAs!=null?` generated always as (${fieldDef.generatedAs}) stored`:'')
|
|
2682
2961
|
);
|
|
@@ -2966,8 +3245,8 @@ AppBackend.prototype.dumpDbSchemaPartial = async function dumpDbSchemaPartial(pa
|
|
|
2966
3245
|
}
|
|
2967
3246
|
}
|
|
2968
3247
|
lines.push(`
|
|
2969
|
-
create or replace
|
|
2970
|
-
security definer
|
|
3248
|
+
create or replace procedure set_app_user(p_username text)
|
|
3249
|
+
security definer language plpgsql
|
|
2971
3250
|
as
|
|
2972
3251
|
$body$
|
|
2973
3252
|
declare
|
|
@@ -2975,7 +3254,7 @@ declare
|
|
|
2975
3254
|
${db.quoteIdent('v_'+fieldName)} text;`
|
|
2976
3255
|
).join('')}
|
|
2977
3256
|
begin
|
|
2978
|
-
if
|
|
3257
|
+
if p_username = '!login' then
|
|
2979
3258
|
${be.config.login.infoFieldList.map(fieldName => `
|
|
2980
3259
|
set backend_plus._${fieldName} = '!';`).join('')}
|
|
2981
3260
|
|
|
@@ -2987,14 +3266,13 @@ begin
|
|
|
2987
3266
|
from ${(be.config.login.from ?? (
|
|
2988
3267
|
(be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':'')+
|
|
2989
3268
|
be.db.quoteIdent(be.config.login.table)))}
|
|
2990
|
-
where ${db.quoteIdent(be.config.login.userFieldName)} =
|
|
3269
|
+
where ${db.quoteIdent(be.config.login.userFieldName)} = p_username;
|
|
2991
3270
|
${be.config.login.infoFieldList.map(fieldName => `
|
|
2992
3271
|
perform set_config('backend_plus._${fieldName}', v_${fieldName}, false);`).join('')}
|
|
2993
3272
|
|
|
2994
3273
|
set backend_plus._mode = normal;
|
|
2995
3274
|
end if;
|
|
2996
|
-
perform set_config('backend_plus._user',
|
|
2997
|
-
return p_user;
|
|
3275
|
+
perform set_config('backend_plus._user', p_username, false);
|
|
2998
3276
|
end;
|
|
2999
3277
|
$body$;
|
|
3000
3278
|
|
|
@@ -3358,8 +3636,10 @@ AppBackend.prototype.exportacionesGenerico = async function exportacionesGeneric
|
|
|
3358
3636
|
}
|
|
3359
3637
|
}
|
|
3360
3638
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3639
|
+
if (!procedureDef.forExport.generarInmediato) {
|
|
3640
|
+
await buscarGenerados(csvFileName);
|
|
3641
|
+
await buscarGenerados(fileName);
|
|
3642
|
+
}
|
|
3363
3643
|
if(hayGenerados.length>0){
|
|
3364
3644
|
return hayGenerados;
|
|
3365
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
|
|
4
|
+
"version": "2.5.2",
|
|
5
5
|
"author": "Codenautas <codenautas@googlegroups.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "codenautas/backend-plus",
|
|
@@ -30,64 +30,65 @@
|
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@upgraded/locate-path": "^6.0.0-alfa.1",
|
|
33
|
-
"ajax-best-promise": "^0.4.
|
|
34
|
-
"backend-skins": "^0.1.
|
|
35
|
-
"best-globals": "^2.0.
|
|
33
|
+
"ajax-best-promise": "^0.4.3",
|
|
34
|
+
"backend-skins": "^0.1.22",
|
|
35
|
+
"best-globals": "^2.0.7",
|
|
36
36
|
"big.js": "^7.0.1",
|
|
37
|
-
"body-parser": "^2.2.
|
|
38
|
-
"cast-error": "^0.1.
|
|
39
|
-
"castellano": "^0.1.
|
|
37
|
+
"body-parser": "^2.2.2",
|
|
38
|
+
"cast-error": "^0.1.3",
|
|
39
|
+
"castellano": "^0.1.5",
|
|
40
40
|
"connect-pg-simple": "^10.0.0",
|
|
41
41
|
"cookie-parser": "^1.4.7",
|
|
42
|
-
"cors": "^2.8.
|
|
43
|
-
"dialog-promise": "^0.10.
|
|
42
|
+
"cors": "^2.8.6",
|
|
43
|
+
"dialog-promise": "^0.10.5",
|
|
44
44
|
"discrepances": "^0.2.9",
|
|
45
|
-
"express": "^5.1
|
|
46
|
-
"express-session": "^1.
|
|
47
|
-
"express-useragent": "^1.0
|
|
48
|
-
"fs-extra": "^11.3.
|
|
49
|
-
"js-to-html": "^1.3.
|
|
50
|
-
"js-yaml": "^4.1.
|
|
51
|
-
"json4all": "^1.4.
|
|
45
|
+
"express": "^5.2.1",
|
|
46
|
+
"express-session": "^1.19.0",
|
|
47
|
+
"express-useragent": "^2.1.0",
|
|
48
|
+
"fs-extra": "^11.3.3",
|
|
49
|
+
"js-to-html": "^1.3.5",
|
|
50
|
+
"js-yaml": "^4.1.1",
|
|
51
|
+
"json4all": "^1.4.2",
|
|
52
52
|
"lazy-some": "^0.1.0",
|
|
53
|
-
"like-ar": "^0.5.
|
|
54
|
-
"login-plus": "^1.
|
|
53
|
+
"like-ar": "^0.5.2",
|
|
54
|
+
"login-plus": "^1.8.1",
|
|
55
55
|
"memorystore": "^1.6.7",
|
|
56
|
-
"mini-tools": "^1.13.
|
|
56
|
+
"mini-tools": "^1.13.5",
|
|
57
57
|
"moment": "^2.30.1",
|
|
58
58
|
"multiparty": "^4.2.3",
|
|
59
|
-
"nodemailer": "^7.0.
|
|
59
|
+
"nodemailer": "^7.0.13",
|
|
60
60
|
"numeral": "^2.0.6",
|
|
61
|
-
"pg-promise-strict": "^1.4.
|
|
62
|
-
"pg-triggers": "0.4.
|
|
61
|
+
"pg-promise-strict": "^1.4.5",
|
|
62
|
+
"pg-triggers": "0.4.6",
|
|
63
63
|
"pikaday": "^1.8.2",
|
|
64
64
|
"pug": "^3.0.3",
|
|
65
65
|
"read-yaml-promise": "^1.0.2",
|
|
66
66
|
"regexplicit": "^0.1.3",
|
|
67
|
-
"require-bro": "^0.3.
|
|
67
|
+
"require-bro": "^0.3.4",
|
|
68
68
|
"self-explain": "^0.11.0",
|
|
69
|
-
"serve-content": "^0.4
|
|
69
|
+
"serve-content": "^1.0.4",
|
|
70
70
|
"session-file-store": "^1.5.0",
|
|
71
|
-
"
|
|
71
|
+
"simple-git": "^3.30.0",
|
|
72
|
+
"sql-tools": "^0.1.7",
|
|
72
73
|
"stack-trace": "^0.0.10",
|
|
73
74
|
"stylus": "0.64.0",
|
|
74
|
-
"type-store": "^0.4.
|
|
75
|
-
"typed-controls": "^0.12.
|
|
75
|
+
"type-store": "^0.4.6",
|
|
76
|
+
"typed-controls": "^0.12.4",
|
|
76
77
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
|
79
80
|
"@types/big.js": "^6.2.2",
|
|
80
81
|
"@types/expect.js": "~0.3.32",
|
|
81
|
-
"@types/express": "^5.0.
|
|
82
|
+
"@types/express": "^5.0.6",
|
|
82
83
|
"@types/express-useragent": "^1.0.5",
|
|
83
84
|
"@types/fs-extra": "^11.0.4",
|
|
84
85
|
"@types/js-yaml": "^4.0.9",
|
|
85
86
|
"@types/mocha": "^10.0.10",
|
|
86
87
|
"@types/multiparty": "~4.2.1",
|
|
87
|
-
"@types/node": "^
|
|
88
|
-
"@types/nodemailer": "^
|
|
88
|
+
"@types/node": "^25.2.0",
|
|
89
|
+
"@types/nodemailer": "^7.0.9",
|
|
89
90
|
"@types/numeral": "~2.0.5",
|
|
90
|
-
"@types/session-file-store": "^1.2.
|
|
91
|
+
"@types/session-file-store": "^1.2.6",
|
|
91
92
|
"@types/stack-trace": "~0.0.33",
|
|
92
93
|
"@types/websql": "~0.0.30",
|
|
93
94
|
"esprima": "^4.0.1",
|
|
@@ -99,20 +100,21 @@
|
|
|
99
100
|
"karma-ie-launcher": "^1.0.0",
|
|
100
101
|
"karma-mocha": "^2.0.1",
|
|
101
102
|
"kill-9": "~0.4.3",
|
|
102
|
-
"mocha": "^11.7.
|
|
103
|
+
"mocha": "^11.7.5",
|
|
103
104
|
"nyc": "^17.1.0",
|
|
104
|
-
"puppeteer": "^24.
|
|
105
|
-
"sinon": "^21.0.
|
|
106
|
-
"supertest": "^7.
|
|
105
|
+
"puppeteer": "^24.36.1",
|
|
106
|
+
"sinon": "^21.0.1",
|
|
107
|
+
"supertest": "^7.2.2",
|
|
107
108
|
"types.d.ts": "~0.6.22",
|
|
108
|
-
"typescript": "^5.
|
|
109
|
+
"typescript": "^5.9.3",
|
|
109
110
|
"why-is-node-running": "^3.2.2"
|
|
110
111
|
},
|
|
111
112
|
"engines": {
|
|
112
113
|
"node": ">= 18"
|
|
113
114
|
},
|
|
114
115
|
"scripts": {
|
|
115
|
-
"test": "
|
|
116
|
+
"test": "echo testear SiPer que usa la mayoría de la funcionalidad de Backend-Plus",
|
|
117
|
+
"test-ui": "(npm run prepublish || echo \"continue w/error\") && mocha --reporter spec --single-run --bail test/test-*.js",
|
|
116
118
|
"test-karma": "(npm run prepublish || echo \"continue w/error\") && mocha --reporter spec --bail test/test-k*.js",
|
|
117
119
|
"test-why": "node --expose-internals ./node_modules/mocha/bin/_mocha --reporter spec --bail test/test*.js",
|
|
118
120
|
"test-ci": "(npm run prepublish || echo \"continue w/error\") && mocha --reporter spec --bail test/test*.js",
|