@futdevpro/nts-dynamo 1.15.17 → 1.15.20
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/_specifications/BACKLOG.md +4 -4
- package/build/_models/interfaces/global-log-settings.interface.d.ts +35 -0
- package/build/_models/interfaces/global-log-settings.interface.d.ts.map +1 -1
- package/build/_modules/admin-auth/_models/admin-api-key-config.interface.d.ts +32 -0
- package/build/_modules/admin-auth/_models/admin-api-key-config.interface.d.ts.map +1 -0
- package/build/_modules/admin-auth/_models/admin-api-key-config.interface.js +3 -0
- package/build/_modules/admin-auth/_models/admin-api-key-config.interface.js.map +1 -0
- package/build/_modules/admin-auth/admin-api-key.auth-service.d.ts +90 -0
- package/build/_modules/admin-auth/admin-api-key.auth-service.d.ts.map +1 -0
- package/build/_modules/admin-auth/admin-api-key.auth-service.js +195 -0
- package/build/_modules/admin-auth/admin-api-key.auth-service.js.map +1 -0
- package/build/_modules/admin-auth/index.d.ts +3 -0
- package/build/_modules/admin-auth/index.d.ts.map +1 -0
- package/build/_modules/admin-auth/index.js +6 -0
- package/build/_modules/admin-auth/index.js.map +1 -0
- package/build/_modules/logs/file-log.service.d.ts +87 -0
- package/build/_modules/logs/file-log.service.d.ts.map +1 -0
- package/build/_modules/logs/file-log.service.js +267 -0
- package/build/_modules/logs/file-log.service.js.map +1 -0
- package/build/_modules/logs/get-logs-routing-module.util.d.ts +19 -0
- package/build/_modules/logs/get-logs-routing-module.util.d.ts.map +1 -0
- package/build/_modules/logs/get-logs-routing-module.util.js +32 -0
- package/build/_modules/logs/get-logs-routing-module.util.js.map +1 -0
- package/build/_modules/logs/index.d.ts +5 -0
- package/build/_modules/logs/index.d.ts.map +1 -0
- package/build/_modules/logs/index.js +12 -0
- package/build/_modules/logs/index.js.map +1 -0
- package/build/_modules/logs/log-buffer.service.d.ts +38 -0
- package/build/_modules/logs/log-buffer.service.d.ts.map +1 -0
- package/build/_modules/logs/log-buffer.service.js +97 -0
- package/build/_modules/logs/log-buffer.service.js.map +1 -0
- package/build/_modules/logs/logs.controller.d.ts +27 -0
- package/build/_modules/logs/logs.controller.d.ts.map +1 -0
- package/build/_modules/logs/logs.controller.js +90 -0
- package/build/_modules/logs/logs.controller.js.map +1 -0
- package/build/_modules/logs/logs.service.d.ts +40 -0
- package/build/_modules/logs/logs.service.d.ts.map +1 -0
- package/build/_modules/logs/logs.service.js +97 -0
- package/build/_modules/logs/logs.service.js.map +1 -0
- package/package.json +1 -1
- package/pipeline.cicd.config.json +3 -1
- package/src/_models/interfaces/global-log-settings.interface.ts +36 -0
- package/src/_modules/admin-auth/_models/admin-api-key-config.interface.ts +33 -0
- package/src/_modules/admin-auth/admin-api-key.auth-service.spec.ts +200 -0
- package/src/_modules/admin-auth/admin-api-key.auth-service.ts +220 -0
- package/src/_modules/admin-auth/index.ts +2 -0
- package/src/_modules/logs/file-log.service.spec.ts +202 -0
- package/src/_modules/logs/file-log.service.ts +283 -0
- package/src/_modules/logs/get-logs-routing-module.util.ts +36 -0
- package/src/_modules/logs/index.ts +4 -0
- package/src/_modules/logs/log-buffer.service.ts +101 -0
- package/src/_modules/logs/logs.controller.ts +109 -0
- package/src/_modules/logs/logs.service.ts +100 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DyNTS_SingletonServiceBase } from '../../_services/base/singleton.service-base';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory ring buffer a szerver logokhoz.
|
|
4
|
+
* Interceptalja a console.log/error/warn kimeneteket es tarolja oket.
|
|
5
|
+
* Max entryszam a log_settings.logs_endpoint.maxEntries-bol jon (default: 2000).
|
|
6
|
+
*
|
|
7
|
+
* Hasznalat:
|
|
8
|
+
* DyNTS_Logs_Service.getInstance().install() — egyszer, szerver indulasakor
|
|
9
|
+
* DyNTS_Logs_Service.getInstance().getLines(200) — utolso N sor
|
|
10
|
+
*/
|
|
11
|
+
export declare class DyNTS_Logs_Service extends DyNTS_SingletonServiceBase {
|
|
12
|
+
static getInstance(): DyNTS_Logs_Service;
|
|
13
|
+
private readonly maxEntries;
|
|
14
|
+
private readonly buffer;
|
|
15
|
+
private installed;
|
|
16
|
+
protected constructor();
|
|
17
|
+
/**
|
|
18
|
+
* Interceptalja a console.log/error/warn kimeneteket es a bufferbe irja.
|
|
19
|
+
* Egyszer kell hivni, a szerver indulasakor. Tobbszori hivas no-op.
|
|
20
|
+
*/
|
|
21
|
+
install(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Az utolso N sor lekerdezese.
|
|
24
|
+
*/
|
|
25
|
+
getLines(count?: number): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Osszes bufferelt sor szama.
|
|
28
|
+
*/
|
|
29
|
+
getLineCount(): number;
|
|
30
|
+
/**
|
|
31
|
+
* Buffer uritese.
|
|
32
|
+
*/
|
|
33
|
+
clear(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Telepitve van-e mar a console intercept.
|
|
36
|
+
*/
|
|
37
|
+
isInstalled(): boolean;
|
|
38
|
+
private addLine;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=logs.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.service.d.ts","sourceRoot":"","sources":["../../../src/_modules/logs/logs.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,6CAA6C,CAAC;AAGzF;;;;;;;;GAQG;AACH,qBAAa,kBAAmB,SAAQ,0BAA0B;IAEhE,MAAM,CAAC,WAAW,IAAI,kBAAkB;IAIxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,SAAS,CAAkB;IAEnC,SAAS;IAKT;;;OAGG;IACH,OAAO,IAAI,IAAI;IA0Bf;;OAEG;IACH,QAAQ,CAAC,KAAK,GAAE,MAAY,GAAG,MAAM,EAAE;IAKvC;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB,OAAO,CAAC,OAAO;CAahB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DyNTS_Logs_Service = void 0;
|
|
4
|
+
const singleton_service_base_1 = require("../../_services/base/singleton.service-base");
|
|
5
|
+
const global_settings_const_1 = require("../../_collections/global-settings.const");
|
|
6
|
+
/**
|
|
7
|
+
* In-memory ring buffer a szerver logokhoz.
|
|
8
|
+
* Interceptalja a console.log/error/warn kimeneteket es tarolja oket.
|
|
9
|
+
* Max entryszam a log_settings.logs_endpoint.maxEntries-bol jon (default: 2000).
|
|
10
|
+
*
|
|
11
|
+
* Hasznalat:
|
|
12
|
+
* DyNTS_Logs_Service.getInstance().install() — egyszer, szerver indulasakor
|
|
13
|
+
* DyNTS_Logs_Service.getInstance().getLines(200) — utolso N sor
|
|
14
|
+
*/
|
|
15
|
+
class DyNTS_Logs_Service extends singleton_service_base_1.DyNTS_SingletonServiceBase {
|
|
16
|
+
static getInstance() {
|
|
17
|
+
return DyNTS_Logs_Service.getSingletonInstance();
|
|
18
|
+
}
|
|
19
|
+
maxEntries;
|
|
20
|
+
buffer = [];
|
|
21
|
+
installed = false;
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
this.maxEntries = global_settings_const_1.DyNTS_global_settings.log_settings?.logs_endpoint?.maxEntries ?? 2000;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Interceptalja a console.log/error/warn kimeneteket es a bufferbe irja.
|
|
28
|
+
* Egyszer kell hivni, a szerver indulasakor. Tobbszori hivas no-op.
|
|
29
|
+
*/
|
|
30
|
+
install() {
|
|
31
|
+
if (this.installed) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.installed = true;
|
|
35
|
+
const originalLog = console.log.bind(console);
|
|
36
|
+
const originalError = console.error.bind(console);
|
|
37
|
+
const originalWarn = console.warn.bind(console);
|
|
38
|
+
const self = this;
|
|
39
|
+
console.log = (...args) => {
|
|
40
|
+
self.addLine('LOG', args);
|
|
41
|
+
originalLog(...args);
|
|
42
|
+
};
|
|
43
|
+
console.error = (...args) => {
|
|
44
|
+
self.addLine('ERR', args);
|
|
45
|
+
originalError(...args);
|
|
46
|
+
};
|
|
47
|
+
console.warn = (...args) => {
|
|
48
|
+
self.addLine('WRN', args);
|
|
49
|
+
originalWarn(...args);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Az utolso N sor lekerdezese.
|
|
54
|
+
*/
|
|
55
|
+
getLines(count = 200) {
|
|
56
|
+
const start = Math.max(0, this.buffer.length - count);
|
|
57
|
+
return this.buffer.slice(start);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Osszes bufferelt sor szama.
|
|
61
|
+
*/
|
|
62
|
+
getLineCount() {
|
|
63
|
+
return this.buffer.length;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Buffer uritese.
|
|
67
|
+
*/
|
|
68
|
+
clear() {
|
|
69
|
+
this.buffer.length = 0;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Telepitve van-e mar a console intercept.
|
|
73
|
+
*/
|
|
74
|
+
isInstalled() {
|
|
75
|
+
return this.installed;
|
|
76
|
+
}
|
|
77
|
+
addLine(level, args) {
|
|
78
|
+
const message = args.map((arg) => {
|
|
79
|
+
if (typeof arg === 'string') {
|
|
80
|
+
return arg;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return JSON.stringify(arg);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return String(arg);
|
|
87
|
+
}
|
|
88
|
+
}).join(' ');
|
|
89
|
+
const timestamped = `${new Date().toISOString()} [${level}] ${message}`;
|
|
90
|
+
this.buffer.push(timestamped);
|
|
91
|
+
if (this.buffer.length > this.maxEntries) {
|
|
92
|
+
this.buffer.shift();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.DyNTS_Logs_Service = DyNTS_Logs_Service;
|
|
97
|
+
//# sourceMappingURL=logs.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.service.js","sourceRoot":"","sources":["../../../src/_modules/logs/logs.service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -105,4 +105,40 @@ export interface DyNTS_GlobalLog_Settings {
|
|
|
105
105
|
/** Max sorok szama a bufferben. Default: 2000. */
|
|
106
106
|
maxEntries?: number;
|
|
107
107
|
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* File-based log kiiras konfiguracio (DyNTS_FileLog_Service).
|
|
111
|
+
*
|
|
112
|
+
* Az in-memory ring buffer (DyNTS_Logs_Service) mellett mukodik — NEM
|
|
113
|
+
* helyette. A szerver stdout/stderr kimeneteket egy per-session log
|
|
114
|
+
* fajlba duplikalja (`{logDir}/{filenamePrefix}YYYY-MM-DD_HH-MM-SS.log`).
|
|
115
|
+
*
|
|
116
|
+
* Rotation: ha az aktiv fajl meghaladja a `maxFileSizeMb`-t, uj fajl jon
|
|
117
|
+
* letre. Retention: az `install()` + minden rotation utan a
|
|
118
|
+
* `maxFiles`-nel regebbi vagy a `retentionDays`-nel idosebb fajlok
|
|
119
|
+
* torlodnek (ami elobb teljesul).
|
|
120
|
+
*
|
|
121
|
+
* Hasznalat:
|
|
122
|
+
* DyNTS_FileLog_Service.getInstance().install(); // szerver startup
|
|
123
|
+
*/
|
|
124
|
+
file_log?: {
|
|
125
|
+
/** Engedelyezve van-e a file-based logolás. Default: false. */
|
|
126
|
+
enabled: boolean;
|
|
127
|
+
/** Log mappa abszolut vagy relativ path-ja. Default: './logs/server'. */
|
|
128
|
+
logDir?: string;
|
|
129
|
+
/** Per-session fajlnev prefix. Default: 'server-'. */
|
|
130
|
+
filenamePrefix?: string;
|
|
131
|
+
/** Rotation trigger MB-ban. Default: 50. */
|
|
132
|
+
maxFileSizeMb?: number;
|
|
133
|
+
/** Retention by count — max ennyi log fajl marad meg. Default: 10. */
|
|
134
|
+
maxFiles?: number;
|
|
135
|
+
/** Retention by age — ennyi napnal regebbi fajl torlodik. Default: 30. */
|
|
136
|
+
retentionDays?: number;
|
|
137
|
+
/** ANSI escape kodok strippelese a fajl-irasnal. Default: true. */
|
|
138
|
+
stripAnsi?: boolean;
|
|
139
|
+
/** stdout interceptalasa. Default: true. */
|
|
140
|
+
includeStdout?: boolean;
|
|
141
|
+
/** stderr interceptalasa. Default: true. */
|
|
142
|
+
includeStderr?: boolean;
|
|
143
|
+
};
|
|
108
144
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config a `DyNTS_AdminApiKey_AuthService.configure(...)`-hoz.
|
|
3
|
+
*
|
|
4
|
+
* Minden mezo opcionalis — a default-ok megfelelnek a tipikus hasznalat-eset
|
|
5
|
+
* elvarasanak (DYNTS_ADMIN_API_KEY env var + x-admin-api-key header +
|
|
6
|
+
* Bearer fallback).
|
|
7
|
+
*/
|
|
8
|
+
export interface DyNTS_AdminApiKey_Config {
|
|
9
|
+
/**
|
|
10
|
+
* Env var nev, ahonnan az admin API key olvasodik.
|
|
11
|
+
* Default: `DYNTS_ADMIN_API_KEY`.
|
|
12
|
+
*
|
|
13
|
+
* Override-olhato pl. multi-tenant deploy-okhoz vagy ha a host app
|
|
14
|
+
* mas konvenciot kovet (`MY_APP_ADMIN_KEY`, stb.).
|
|
15
|
+
*/
|
|
16
|
+
envVarName?: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* HTTP header nev (case-insensitive — Express normalizalja lowercase-re).
|
|
20
|
+
* Default: `x-admin-api-key`.
|
|
21
|
+
*/
|
|
22
|
+
headerName?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Engedi-e az `Authorization: Bearer <key>` fallback-et a primer header
|
|
26
|
+
* helyett. Default: `true`.
|
|
27
|
+
*
|
|
28
|
+
* Hasznos amikor a kliens egy generikus HTTP klienst hasznal ami csak
|
|
29
|
+
* az Authorization header-t allitja, vagy amikor proxy-k strippelik
|
|
30
|
+
* a custom header-eket.
|
|
31
|
+
*/
|
|
32
|
+
allowAuthorizationBearer?: boolean;
|
|
33
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
|
|
3
|
+
import { DyFM_Error } from '@futdevpro/fsm-dynamo';
|
|
4
|
+
|
|
5
|
+
import { DyNTS_AdminApiKey_AuthService } from './admin-api-key.auth-service';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const TEST_KEY: string = 'super-secret-admin-key-1234567890';
|
|
9
|
+
const TEST_ENV_VAR: string = 'DYNTS_ADMIN_API_KEY';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/** Test-only — minimal Request mock-ot ad vissza adott headers-szel. */
|
|
13
|
+
const mockReq = (headers: Record<string, string | undefined> = {}): Request => {
|
|
14
|
+
return { headers: headers } as unknown as Request;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Test-only — minimal Response stub (verify nem irja, csak typing miatt kell). */
|
|
18
|
+
const mockRes = (): Response => {
|
|
19
|
+
return {} as unknown as Response;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
describe('| DyNTS_AdminApiKey_AuthService', (): void => {
|
|
24
|
+
let svc: DyNTS_AdminApiKey_AuthService;
|
|
25
|
+
let originalEnv: string | undefined;
|
|
26
|
+
|
|
27
|
+
beforeEach((): void => {
|
|
28
|
+
svc = DyNTS_AdminApiKey_AuthService.getInstance();
|
|
29
|
+
svc._resetForTesting();
|
|
30
|
+
originalEnv = process.env[TEST_ENV_VAR];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach((): void => {
|
|
34
|
+
svc._resetForTesting();
|
|
35
|
+
if (originalEnv === undefined) {
|
|
36
|
+
delete process.env[TEST_ENV_VAR];
|
|
37
|
+
} else {
|
|
38
|
+
process.env[TEST_ENV_VAR] = originalEnv;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
describe('| verify() — config errors', (): void => {
|
|
44
|
+
it('| 500 ha az env var nincs beallitva', async (): Promise<void> => {
|
|
45
|
+
delete process.env[TEST_ENV_VAR];
|
|
46
|
+
let thrown: any = null;
|
|
47
|
+
try { await svc.verify(mockReq({}), mockRes()); } catch (e) { thrown = e; }
|
|
48
|
+
expect(thrown).not.toBeNull();
|
|
49
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(500);
|
|
50
|
+
expect(DyFM_Error.getErrorCode(thrown)).toContain('DyNTS-AAK-CONFIG');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('| 500 ha az env var ures string', async (): Promise<void> => {
|
|
54
|
+
process.env[TEST_ENV_VAR] = '';
|
|
55
|
+
let thrown: any = null;
|
|
56
|
+
try { await svc.verify(mockReq({}), mockRes()); } catch (e) { thrown = e; }
|
|
57
|
+
expect(thrown).not.toBeNull();
|
|
58
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(500);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('| verify() — auth errors', (): void => {
|
|
63
|
+
beforeEach((): void => { process.env[TEST_ENV_VAR] = TEST_KEY; });
|
|
64
|
+
|
|
65
|
+
it('| 401 ha a header teljesen hianyzik', async (): Promise<void> => {
|
|
66
|
+
let thrown: any = null;
|
|
67
|
+
try { await svc.verify(mockReq({}), mockRes()); } catch (e) { thrown = e; }
|
|
68
|
+
expect(thrown).not.toBeNull();
|
|
69
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(401);
|
|
70
|
+
expect(DyFM_Error.getErrorCode(thrown)).toContain('DyNTS-AAK-MISSING');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('| 401 ha a header rossz erteku', async (): Promise<void> => {
|
|
74
|
+
let thrown: any = null;
|
|
75
|
+
try {
|
|
76
|
+
await svc.verify(mockReq({ 'x-admin-api-key': 'wrong-key' }), mockRes());
|
|
77
|
+
} catch (e) { thrown = e; }
|
|
78
|
+
expect(thrown).not.toBeNull();
|
|
79
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(401);
|
|
80
|
+
expect(DyFM_Error.getErrorCode(thrown)).toContain('DyNTS-AAK-INVALID');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('| 401 length-mismatch eseten is (timing-safe path)', async (): Promise<void> => {
|
|
84
|
+
let thrown: any = null;
|
|
85
|
+
try {
|
|
86
|
+
await svc.verify(mockReq({ 'x-admin-api-key': 'short' }), mockRes());
|
|
87
|
+
} catch (e) { thrown = e; }
|
|
88
|
+
expect(thrown).not.toBeNull();
|
|
89
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(401);
|
|
90
|
+
expect(DyFM_Error.getErrorCode(thrown)).toContain('DyNTS-AAK-INVALID');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('| verify() — happy paths', (): void => {
|
|
95
|
+
beforeEach((): void => { process.env[TEST_ENV_VAR] = TEST_KEY; });
|
|
96
|
+
|
|
97
|
+
it('| silent pass ha az x-admin-api-key header egyezik', async (): Promise<void> => {
|
|
98
|
+
await expectAsync(
|
|
99
|
+
svc.verify(mockReq({ 'x-admin-api-key': TEST_KEY }), mockRes()),
|
|
100
|
+
).toBeResolved();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('| silent pass Authorization Bearer fallback-bol (default engedelyezve)', async (): Promise<void> => {
|
|
104
|
+
await expectAsync(
|
|
105
|
+
svc.verify(mockReq({ authorization: `Bearer ${TEST_KEY}` }), mockRes()),
|
|
106
|
+
).toBeResolved();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('| silent pass Authorization Bearer kis-nagybetu case-insensitive', async (): Promise<void> => {
|
|
110
|
+
await expectAsync(
|
|
111
|
+
svc.verify(mockReq({ authorization: `bearer ${TEST_KEY}` }), mockRes()),
|
|
112
|
+
).toBeResolved();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('| configure() — overrides', (): void => {
|
|
117
|
+
it('| custom envVarName-bol olvas', async (): Promise<void> => {
|
|
118
|
+
svc.configure({ envVarName: 'MY_CUSTOM_KEY' });
|
|
119
|
+
process.env['MY_CUSTOM_KEY'] = TEST_KEY;
|
|
120
|
+
|
|
121
|
+
await expectAsync(
|
|
122
|
+
svc.verify(mockReq({ 'x-admin-api-key': TEST_KEY }), mockRes()),
|
|
123
|
+
).toBeResolved();
|
|
124
|
+
|
|
125
|
+
delete process.env['MY_CUSTOM_KEY'];
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('| custom headerName-rol olvas', async (): Promise<void> => {
|
|
129
|
+
process.env[TEST_ENV_VAR] = TEST_KEY;
|
|
130
|
+
svc.configure({ headerName: 'x-my-admin' });
|
|
131
|
+
|
|
132
|
+
// Default x-admin-api-key MOSTANTOL NEM mukodik
|
|
133
|
+
let thrown: any = null;
|
|
134
|
+
try {
|
|
135
|
+
await svc.verify(mockReq({ 'x-admin-api-key': TEST_KEY }), mockRes());
|
|
136
|
+
} catch (e) { thrown = e; }
|
|
137
|
+
expect(thrown).not.toBeNull();
|
|
138
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(401);
|
|
139
|
+
|
|
140
|
+
// Az uj header viszont igen
|
|
141
|
+
await expectAsync(
|
|
142
|
+
svc.verify(mockReq({ 'x-my-admin': TEST_KEY }), mockRes()),
|
|
143
|
+
).toBeResolved();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('| allowAuthorizationBearer=false letiltja a Bearer fallback-et', async (): Promise<void> => {
|
|
147
|
+
process.env[TEST_ENV_VAR] = TEST_KEY;
|
|
148
|
+
svc.configure({ allowAuthorizationBearer: false });
|
|
149
|
+
|
|
150
|
+
let thrown: any = null;
|
|
151
|
+
try {
|
|
152
|
+
await svc.verify(mockReq({ authorization: `Bearer ${TEST_KEY}` }), mockRes());
|
|
153
|
+
} catch (e) { thrown = e; }
|
|
154
|
+
expect(thrown).not.toBeNull();
|
|
155
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(401);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('| configure() partial override — a tobbi mezo default marad', (): void => {
|
|
159
|
+
svc.configure({ headerName: 'x-only-header' });
|
|
160
|
+
const config = svc.getConfig();
|
|
161
|
+
expect(config.headerName).toBe('x-only-header');
|
|
162
|
+
expect(config.envVarName).toBe('DYNTS_ADMIN_API_KEY');
|
|
163
|
+
expect(config.allowAuthorizationBearer).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('| configure() headerName lowercase-re normalizal', (): void => {
|
|
167
|
+
svc.configure({ headerName: 'X-Mixed-Case-Header' });
|
|
168
|
+
expect(svc.getConfig().headerName).toBe('x-mixed-case-header');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('| env-read-on-each-call (dynamic)', (): void => {
|
|
173
|
+
it('| env var update kozben hat: install-utani env-modositas ervenyes', async (): Promise<void> => {
|
|
174
|
+
delete process.env[TEST_ENV_VAR];
|
|
175
|
+
// Elso hivas: env hianyzik → 500
|
|
176
|
+
let thrown: any = null;
|
|
177
|
+
try { await svc.verify(mockReq({}), mockRes()); } catch (e) { thrown = e; }
|
|
178
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(500);
|
|
179
|
+
|
|
180
|
+
// Most allitsuk be az env-et
|
|
181
|
+
process.env[TEST_ENV_VAR] = TEST_KEY;
|
|
182
|
+
|
|
183
|
+
// Masodik hivas helyes header-rel → pass
|
|
184
|
+
await expectAsync(
|
|
185
|
+
svc.verify(mockReq({ 'x-admin-api-key': TEST_KEY }), mockRes()),
|
|
186
|
+
).toBeResolved();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('| .verify binding', (): void => {
|
|
191
|
+
it('| verify atadhato preProcesses-be detached method-kent (this-binding megtarttva)', async (): Promise<void> => {
|
|
192
|
+
process.env[TEST_ENV_VAR] = TEST_KEY;
|
|
193
|
+
const detached: (req: Request, res: Response) => Promise<void> = svc.verify;
|
|
194
|
+
// NEM call-ban svc.verify(...) hanem szabad fuggveny-kent — bindelt this-re kell hagyatkozni
|
|
195
|
+
await expectAsync(
|
|
196
|
+
detached(mockReq({ 'x-admin-api-key': TEST_KEY }), mockRes()),
|
|
197
|
+
).toBeResolved();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
|
|
4
|
+
import { DyFM_Error } from '@futdevpro/fsm-dynamo';
|
|
5
|
+
|
|
6
|
+
import { DyNTS_SingletonServiceBase } from '../../_services/base/singleton.service-base';
|
|
7
|
+
import { DyNTS_global_settings } from '../../_collections/global-settings.const';
|
|
8
|
+
|
|
9
|
+
import { DyNTS_AdminApiKey_Config } from './_models/admin-api-key-config.interface';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/** Default env var nev az admin API key-hez. */
|
|
13
|
+
const DEFAULT_ENV_VAR_NAME: string = 'DYNTS_ADMIN_API_KEY';
|
|
14
|
+
|
|
15
|
+
/** Default header nev (Express lowercase-re normalizalja az osszes header-t). */
|
|
16
|
+
const DEFAULT_HEADER_NAME: string = 'x-admin-api-key';
|
|
17
|
+
|
|
18
|
+
/** Default Bearer fallback engedelyezve van. */
|
|
19
|
+
const DEFAULT_ALLOW_BEARER: boolean = true;
|
|
20
|
+
|
|
21
|
+
/** Service-nev az error-okhoz. */
|
|
22
|
+
const SERVICE_NAME: string = 'DyNTS_AdminApiKey_AuthService';
|
|
23
|
+
|
|
24
|
+
/** ErrorCode prefix — system shortcode + saját kod. */
|
|
25
|
+
const buildErrorCode = (subcode: string): string => {
|
|
26
|
+
const sys: string = DyNTS_global_settings.systemShortCodeName ?? 'DyNTS';
|
|
27
|
+
return `${sys}|DyNTS-AAK-${subcode}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Admin API key auth service — opt-in HTTP guard a meglevo `DyNTS_Endpoint_Params.preProcesses`
|
|
33
|
+
* mechanizmushoz. Egy env var-ban tarolt fix kulccsal valid-alja a bejovo kerest.
|
|
34
|
+
*
|
|
35
|
+
* **Hasznalat (host app):**
|
|
36
|
+
* ```ts
|
|
37
|
+
* const adminAuth = DyNTS_AdminApiKey_AuthService.getInstance();
|
|
38
|
+
* // opcionalis konfig:
|
|
39
|
+
* // adminAuth.configure({ envVarName: 'MY_KEY', headerName: 'x-my-key' });
|
|
40
|
+
*
|
|
41
|
+
* new DyNTS_Endpoint_Params({
|
|
42
|
+
* ...,
|
|
43
|
+
* preProcesses: [adminAuth.verify, ...other],
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* // vagy a logs routing module-on at
|
|
47
|
+
* DyNTS_getLogsRoutingModule({ authPreProcess: adminAuth.verify });
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* **Viselkedes:**
|
|
51
|
+
* - env var beallitva ES helyes header → silent pass
|
|
52
|
+
* - env var beallitva, header hianyzik / rossz → 401 DyFM_Error
|
|
53
|
+
* - env var NINCS beallitva → 500 DyFM_Error (fail-closed; NEM silent allow)
|
|
54
|
+
*
|
|
55
|
+
* **Header lookup:**
|
|
56
|
+
* 1. `x-admin-api-key` (default canonical header)
|
|
57
|
+
* 2. `Authorization: Bearer <key>` (fallback ha `allowAuthorizationBearer === true`)
|
|
58
|
+
*
|
|
59
|
+
* **Timing-safe:** `crypto.timingSafeEqual` Buffer-konvertalassal. Length-mismatch
|
|
60
|
+
* eseten dummy compare-rel azonos idő, hogy a kulcs-hossz ne szivarogjon ki.
|
|
61
|
+
*
|
|
62
|
+
* **Env var read-on-each-call:** a `verify()` minden hivasnal olvassa az env-et,
|
|
63
|
+
* nem cache-eli. Igy a host az env-et utolagosan is allithatja (pl. config
|
|
64
|
+
* loader az auth.service.install() utan).
|
|
65
|
+
*
|
|
66
|
+
* **Singleton:** `getInstance()`-szel hivd. A `.verify` mezo binding-elve van
|
|
67
|
+
* `this`-re, igy direkt atadhato `preProcesses`-be ujracsomagolas nelkul.
|
|
68
|
+
*/
|
|
69
|
+
export class DyNTS_AdminApiKey_AuthService extends DyNTS_SingletonServiceBase {
|
|
70
|
+
|
|
71
|
+
static getInstance(): DyNTS_AdminApiKey_AuthService {
|
|
72
|
+
return DyNTS_AdminApiKey_AuthService.getSingletonInstance() as DyNTS_AdminApiKey_AuthService;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private envVarName: string = DEFAULT_ENV_VAR_NAME;
|
|
76
|
+
private headerName: string = DEFAULT_HEADER_NAME;
|
|
77
|
+
private allowAuthorizationBearer: boolean = DEFAULT_ALLOW_BEARER;
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Konfig override. Hianyzo mezok a default-okat orzik.
|
|
82
|
+
* Hivhato barmikor — a `verify()` a friss config-ot olvassa.
|
|
83
|
+
*/
|
|
84
|
+
configure(config: DyNTS_AdminApiKey_Config): void {
|
|
85
|
+
if (config.envVarName !== undefined) {
|
|
86
|
+
this.envVarName = config.envVarName;
|
|
87
|
+
}
|
|
88
|
+
if (config.headerName !== undefined) {
|
|
89
|
+
// Express lowercase-re normalizal — itt is lowercase-eljuk a konzisztenciaert
|
|
90
|
+
this.headerName = config.headerName.toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
if (config.allowAuthorizationBearer !== undefined) {
|
|
93
|
+
this.allowAuthorizationBearer = config.allowAuthorizationBearer;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Aktualis konfig olvasasa (test/diagnosztika celokra).
|
|
99
|
+
*/
|
|
100
|
+
getConfig(): Required<DyNTS_AdminApiKey_Config> {
|
|
101
|
+
return {
|
|
102
|
+
envVarName: this.envVarName,
|
|
103
|
+
headerName: this.headerName,
|
|
104
|
+
allowAuthorizationBearer: this.allowAuthorizationBearer,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Pre-process function — atadhato `DyNTS_Endpoint_Params.preProcesses`-be,
|
|
110
|
+
* vagy `DyNTS_getLogsRoutingModule({ authPreProcess: ... })`-be.
|
|
111
|
+
*
|
|
112
|
+
* Throws:
|
|
113
|
+
* - 500 ha az env var nincs beallitva (vagy ures string)
|
|
114
|
+
* - 401 ha a header hianyzik vagy nem egyezik
|
|
115
|
+
*
|
|
116
|
+
* A `req`/`res` parametereket NEM modositja (a kerest tovabb engedi a tovabbi
|
|
117
|
+
* preProcess-eknek; csak hiba eseten throw-ol).
|
|
118
|
+
*/
|
|
119
|
+
readonly verify = async (req: Request, _res: Response): Promise<void> => {
|
|
120
|
+
const expectedKey: string = process.env[this.envVarName] ?? '';
|
|
121
|
+
if (expectedKey.length === 0) {
|
|
122
|
+
throw new DyFM_Error({
|
|
123
|
+
status: 500,
|
|
124
|
+
errorCode: buildErrorCode('CONFIG'),
|
|
125
|
+
addECToUserMsg: true,
|
|
126
|
+
message: `Admin API key not configured: env var ${this.envVarName} is not set or empty`,
|
|
127
|
+
userMessage: 'Server configuration error',
|
|
128
|
+
issuerService: SERVICE_NAME,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const providedKey: string | null = this.extractKeyFromRequest(req);
|
|
133
|
+
if (providedKey === null) {
|
|
134
|
+
throw new DyFM_Error({
|
|
135
|
+
status: 401,
|
|
136
|
+
errorCode: buildErrorCode('MISSING'),
|
|
137
|
+
addECToUserMsg: true,
|
|
138
|
+
message: `Admin API key required (expected header: ${this.headerName})`,
|
|
139
|
+
userMessage: 'Admin API key required',
|
|
140
|
+
issuerService: SERVICE_NAME,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!this.timingSafeEquals(providedKey, expectedKey)) {
|
|
145
|
+
throw new DyFM_Error({
|
|
146
|
+
status: 401,
|
|
147
|
+
errorCode: buildErrorCode('INVALID'),
|
|
148
|
+
addECToUserMsg: true,
|
|
149
|
+
message: 'Admin API key invalid',
|
|
150
|
+
userMessage: 'Admin API key invalid',
|
|
151
|
+
issuerService: SERVICE_NAME,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Silent pass — return resolved promise
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Header lookup — elobb a primer header, aztan opcionalisan az Authorization Bearer.
|
|
161
|
+
* Az ures string is "hianyzo"-nak szamit (a Buffer.from('') es timingSafeEqual
|
|
162
|
+
* konzisztencia miatt).
|
|
163
|
+
*/
|
|
164
|
+
private extractKeyFromRequest(req: Request): string | null {
|
|
165
|
+
// Primer header
|
|
166
|
+
const primary: unknown = req.headers[this.headerName];
|
|
167
|
+
const primaryStr: string = Array.isArray(primary) ? primary[0] ?? '' : (typeof primary === 'string' ? primary : '');
|
|
168
|
+
if (primaryStr.length > 0) {
|
|
169
|
+
return primaryStr;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Authorization Bearer fallback
|
|
173
|
+
if (this.allowAuthorizationBearer) {
|
|
174
|
+
const authHeader: unknown = req.headers['authorization'];
|
|
175
|
+
const authStr: string = Array.isArray(authHeader) ? authHeader[0] ?? '' : (typeof authHeader === 'string' ? authHeader : '');
|
|
176
|
+
if (authStr.toLowerCase().startsWith('bearer ')) {
|
|
177
|
+
const token: string = authStr.substring(7).trim();
|
|
178
|
+
if (token.length > 0) {
|
|
179
|
+
return token;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Timing-safe compare ket string kozott. Length-mismatch eseten egy dummy
|
|
189
|
+
* compare-rel azonos idot kenyszeritunk (a kulcs-hossz nem szivaroghat ki
|
|
190
|
+
* timing-attackal).
|
|
191
|
+
*
|
|
192
|
+
* crypto.timingSafeEqual KOTELEZOEN azonos Buffer-hosszt var — kulonbozo
|
|
193
|
+
* hosszra throw-ol, ezert vizsgaljuk elobb a length-et es csak utana
|
|
194
|
+
* compare-elunk.
|
|
195
|
+
*/
|
|
196
|
+
private timingSafeEquals(a: string, b: string): boolean {
|
|
197
|
+
const aBuf: Buffer = Buffer.from(a, 'utf-8');
|
|
198
|
+
const bBuf: Buffer = Buffer.from(b, 'utf-8');
|
|
199
|
+
|
|
200
|
+
if (aBuf.length !== bBuf.length) {
|
|
201
|
+
// Dummy compare ugyanazzal a string-gel: konstans ideju mukodest biztosit
|
|
202
|
+
// mielott visszaternenk false-szal — igy a length-mismatch nem szivaroghat ki.
|
|
203
|
+
crypto.timingSafeEqual(bBuf, bBuf);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return crypto.timingSafeEqual(aBuf, bBuf);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Test-only: visszaallitja a default config-ot, hogy a specfajlok ne szivarogjak
|
|
213
|
+
* at egymas state-jet. Production code NE hivja.
|
|
214
|
+
*/
|
|
215
|
+
_resetForTesting(): void {
|
|
216
|
+
this.envVarName = DEFAULT_ENV_VAR_NAME;
|
|
217
|
+
this.headerName = DEFAULT_HEADER_NAME;
|
|
218
|
+
this.allowAuthorizationBearer = DEFAULT_ALLOW_BEARER;
|
|
219
|
+
}
|
|
220
|
+
}
|