emilsoftware-utilities 2.0.0-dev.82 → 2.0.0-dev.83
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/dist/accessi/server/index.d.ts +1 -0
- package/dist/accessi/server/index.js +1 -0
- package/dist/accessi/server/index.js.map +1 -1
- package/dist/accessi/server/tenantSupervisor.d.ts +60 -0
- package/dist/accessi/server/tenantSupervisor.js +296 -0
- package/dist/accessi/server/tenantSupervisor.js.map +1 -0
- package/dist/accessi/server/tenants.js +8 -0
- package/dist/accessi/server/tenants.js.map +1 -1
- package/dist/accessi-module/Console/console.js +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.83.tgz +0 -0
- package/docs/adr/0002-multi-tenancy.md +4 -4
- package/docs/multi-tenancy.md +21 -3
- package/package.json +3 -3
- package/dist/emilsoftware-utilities-2.0.0-dev.82.tgz +0 -0
|
@@ -9,5 +9,6 @@ export * from "../../accessi-module";
|
|
|
9
9
|
export * from "./standalone";
|
|
10
10
|
export * from "./tenants";
|
|
11
11
|
export * from "./tenantStores";
|
|
12
|
+
export * from "./tenantSupervisor";
|
|
12
13
|
export type { AccessiOptions as AccessiServerOptions } from "../../accessi-module";
|
|
13
14
|
export { createAccessiOptionsFromEnv, createCatalogScriptsOptionsFromEnv, parseAccessiPreviousPublicKeys, } from "../../accessi-module/database-updates/accessiOptionsFromEnv";
|
|
@@ -26,6 +26,7 @@ __exportStar(require("../../accessi-module"), exports);
|
|
|
26
26
|
__exportStar(require("./standalone"), exports);
|
|
27
27
|
__exportStar(require("./tenants"), exports);
|
|
28
28
|
__exportStar(require("./tenantStores"), exports);
|
|
29
|
+
__exportStar(require("./tenantSupervisor"), exports);
|
|
29
30
|
var accessiOptionsFromEnv_1 = require("../../accessi-module/database-updates/accessiOptionsFromEnv");
|
|
30
31
|
Object.defineProperty(exports, "createAccessiOptionsFromEnv", { enumerable: true, get: function () { return accessiOptionsFromEnv_1.createAccessiOptionsFromEnv; } });
|
|
31
32
|
Object.defineProperty(exports, "createCatalogScriptsOptionsFromEnv", { enumerable: true, get: function () { return accessiOptionsFromEnv_1.createCatalogScriptsOptionsFromEnv; } });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/accessi/server/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA;;;;;;GAMG;AACH,uDAAqC;AACrC,+CAA6B;AAC7B,4CAA0B;AAC1B,iDAA+B;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/accessi/server/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA;;;;;;GAMG;AACH,uDAAqC;AACrC,+CAA6B;AAC7B,4CAA0B;AAC1B,iDAA+B;AAC/B,qDAAmC;AAEnC,qGAIqE;AAHnE,oIAAA,2BAA2B,OAAA;AAC3B,2IAAA,kCAAkC,OAAA;AAClC,uIAAA,8BAA8B,OAAA","sourcesContent":["/**\n * Entry point server Accessi.\n *\n * Questo e l'unico entry point che puo importare database, SMTP, migrazioni,\n * console e servizi Nest completi. I backend fratelli devono usare\n * `accessi/resource-server` e `accessi/client`.\n */\nexport * from \"../../accessi-module\";\nexport * from \"./standalone\";\nexport * from \"./tenants\";\nexport * from \"./tenantStores\";\nexport * from \"./tenantSupervisor\";\nexport type { AccessiOptions as AccessiServerOptions } from \"../../accessi-module\";\nexport {\n createAccessiOptionsFromEnv,\n createCatalogScriptsOptionsFromEnv,\n parseAccessiPreviousPublicKeys,\n} from \"../../accessi-module/database-updates/accessiOptionsFromEnv\";\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AccessiTenant } from './tenants';
|
|
2
|
+
/** Handle minimo di un processo figlio (compatibile con `child_process.ChildProcess`). */
|
|
3
|
+
export interface AccessiTenantProcessHandle {
|
|
4
|
+
readonly pid?: number;
|
|
5
|
+
kill(signal?: NodeJS.Signals | number): boolean;
|
|
6
|
+
once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
7
|
+
}
|
|
8
|
+
export type AccessiTenantSpawn = (tenant: AccessiTenant, port: number) => AccessiTenantProcessHandle;
|
|
9
|
+
export interface AccessiTenantSupervisorLoggerEvent {
|
|
10
|
+
level: 'info' | 'warn' | 'error';
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AccessiTenantSupervisorOptions {
|
|
14
|
+
directory: string;
|
|
15
|
+
basePort: number;
|
|
16
|
+
spawn: AccessiTenantSpawn;
|
|
17
|
+
allowHeader?: boolean;
|
|
18
|
+
/** Intervallo di polling della cartella quando `watch()` e attivo. Default 1000 ms. */
|
|
19
|
+
pollIntervalMs?: number;
|
|
20
|
+
/** Attesa prima di applicare un reload dopo una modifica. Default 200 ms. */
|
|
21
|
+
debounceMs?: number;
|
|
22
|
+
/** Timeout per SIGTERM prima di SIGKILL. Default 5000 ms. */
|
|
23
|
+
stopTimeoutMs?: number;
|
|
24
|
+
logger?: (event: AccessiTenantSupervisorLoggerEvent) => void;
|
|
25
|
+
}
|
|
26
|
+
export interface AccessiTenantSupervisorState {
|
|
27
|
+
id: string;
|
|
28
|
+
port: number;
|
|
29
|
+
file: string;
|
|
30
|
+
signature: string;
|
|
31
|
+
}
|
|
32
|
+
export interface AccessiTenantReloadResult {
|
|
33
|
+
added: string[];
|
|
34
|
+
removed: string[];
|
|
35
|
+
restarted: string[];
|
|
36
|
+
unchanged: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface AccessiTenantSupervisor {
|
|
39
|
+
start(): Promise<void>;
|
|
40
|
+
reload(): Promise<AccessiTenantReloadResult>;
|
|
41
|
+
stop(): Promise<void>;
|
|
42
|
+
state(): AccessiTenantSupervisorState[];
|
|
43
|
+
watch(): void;
|
|
44
|
+
unwatch(): void;
|
|
45
|
+
}
|
|
46
|
+
/** Firma della cartella: file + contenuto, ordinata per nome. */
|
|
47
|
+
export declare function accessiTenantDirectorySignature(directory: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Supervisore multi-tenant: avvia un processo per tenant e, con `watch()`,
|
|
50
|
+
* riapplica la configurazione quando i file `*.environment.ts` cambiano.
|
|
51
|
+
*
|
|
52
|
+
* Regole di reload:
|
|
53
|
+
* - un file invalido non tocca i tenant attivi;
|
|
54
|
+
* - i tenant invariati non vengono riavviati;
|
|
55
|
+
* - aggiunte/rimozioni/modifiche producono spawn/stop/restart;
|
|
56
|
+
* - la porta di un tenant esistente non cambia durante un restart.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createAccessiTenantSupervisor(options: AccessiTenantSupervisorOptions): AccessiTenantSupervisor;
|
|
59
|
+
/** Attende che una condizione diventi vera (utile nei test e nei runbook). */
|
|
60
|
+
export declare function waitForTenantState(supervisor: AccessiTenantSupervisor, predicate: (state: AccessiTenantSupervisorState[]) => boolean, timeoutMs?: number): Promise<void>;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.accessiTenantDirectorySignature = accessiTenantDirectorySignature;
|
|
13
|
+
exports.createAccessiTenantSupervisor = createAccessiTenantSupervisor;
|
|
14
|
+
exports.waitForTenantState = waitForTenantState;
|
|
15
|
+
const crypto_1 = require("crypto");
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const path_1 = require("path");
|
|
18
|
+
const tenants_1 = require("./tenants");
|
|
19
|
+
const ENVIRONMENT_FILE = /\.environment\.(ts|mts|cts|js|mjs|cjs)$/;
|
|
20
|
+
function fileSignature(file) {
|
|
21
|
+
try {
|
|
22
|
+
return (0, crypto_1.createHash)('sha256').update((0, fs_1.readFileSync)(file)).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
catch (_a) {
|
|
25
|
+
return 'unreadable';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Firma della cartella: file + contenuto, ordinata per nome. */
|
|
29
|
+
function accessiTenantDirectorySignature(directory) {
|
|
30
|
+
let files;
|
|
31
|
+
try {
|
|
32
|
+
files = (0, fs_1.readdirSync)(directory, { withFileTypes: true })
|
|
33
|
+
.filter((entry) => entry.isFile() && ENVIRONMENT_FILE.test(entry.name))
|
|
34
|
+
.map((entry) => entry.name)
|
|
35
|
+
.sort();
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return `error:${error instanceof Error ? error.message : String(error)}`;
|
|
39
|
+
}
|
|
40
|
+
return (0, crypto_1.createHash)('sha256')
|
|
41
|
+
.update(files.map((file) => `${file}:${fileSignature((0, path_1.join)(directory, file))}`).join('|'))
|
|
42
|
+
.digest('hex');
|
|
43
|
+
}
|
|
44
|
+
function delay(ms) {
|
|
45
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
46
|
+
}
|
|
47
|
+
function stopProcess(handle, timeoutMs) {
|
|
48
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
49
|
+
yield new Promise((resolve) => {
|
|
50
|
+
let settled = false;
|
|
51
|
+
const done = () => {
|
|
52
|
+
if (settled)
|
|
53
|
+
return;
|
|
54
|
+
settled = true;
|
|
55
|
+
resolve();
|
|
56
|
+
};
|
|
57
|
+
handle.once('exit', done);
|
|
58
|
+
try {
|
|
59
|
+
if (!handle.kill('SIGTERM'))
|
|
60
|
+
done();
|
|
61
|
+
}
|
|
62
|
+
catch (_a) {
|
|
63
|
+
done();
|
|
64
|
+
}
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
if (settled)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
handle.kill('SIGKILL');
|
|
70
|
+
}
|
|
71
|
+
catch (_a) {
|
|
72
|
+
// Il processo potrebbe essere gia terminato.
|
|
73
|
+
}
|
|
74
|
+
done();
|
|
75
|
+
}, Math.max(50, timeoutMs));
|
|
76
|
+
if (typeof timer.unref === 'function')
|
|
77
|
+
timer.unref();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Supervisore multi-tenant: avvia un processo per tenant e, con `watch()`,
|
|
83
|
+
* riapplica la configurazione quando i file `*.environment.ts` cambiano.
|
|
84
|
+
*
|
|
85
|
+
* Regole di reload:
|
|
86
|
+
* - un file invalido non tocca i tenant attivi;
|
|
87
|
+
* - i tenant invariati non vengono riavviati;
|
|
88
|
+
* - aggiunte/rimozioni/modifiche producono spawn/stop/restart;
|
|
89
|
+
* - la porta di un tenant esistente non cambia durante un restart.
|
|
90
|
+
*/
|
|
91
|
+
function createAccessiTenantSupervisor(options) {
|
|
92
|
+
var _a;
|
|
93
|
+
if (!(options === null || options === void 0 ? void 0 : options.directory))
|
|
94
|
+
throw new Error('createAccessiTenantSupervisor: directory obbligatoria.');
|
|
95
|
+
if (!Number.isInteger(options.basePort) || options.basePort < 1) {
|
|
96
|
+
throw new Error('createAccessiTenantSupervisor: basePort non valida.');
|
|
97
|
+
}
|
|
98
|
+
if (typeof options.spawn !== 'function') {
|
|
99
|
+
throw new Error('createAccessiTenantSupervisor: spawn obbligatorio.');
|
|
100
|
+
}
|
|
101
|
+
const log = (_a = options.logger) !== null && _a !== void 0 ? _a : (() => undefined);
|
|
102
|
+
const running = new Map();
|
|
103
|
+
let lastSignature = '';
|
|
104
|
+
let watchTimer;
|
|
105
|
+
let reloading = false;
|
|
106
|
+
let stopping = false;
|
|
107
|
+
let pendingReload;
|
|
108
|
+
const nextPort = () => {
|
|
109
|
+
const used = new Set([...running.values()].map((entry) => entry.port));
|
|
110
|
+
let port = options.basePort;
|
|
111
|
+
while (used.has(port))
|
|
112
|
+
port += 1;
|
|
113
|
+
return port;
|
|
114
|
+
};
|
|
115
|
+
const spawnTenant = (tenant, port, signature) => {
|
|
116
|
+
try {
|
|
117
|
+
const process = options.spawn(tenant, port);
|
|
118
|
+
return { tenant, port, signature, process };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
log({
|
|
122
|
+
level: 'error',
|
|
123
|
+
message: `Avvio tenant ${tenant.id} fallito: ${error instanceof Error ? error.message : String(error)}`,
|
|
124
|
+
});
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const load = () => {
|
|
129
|
+
const registry = (0, tenants_1.loadAccessiTenants)(Object.assign({ directory: options.directory }, (options.allowHeader !== undefined ? { allowHeader: options.allowHeader } : {})));
|
|
130
|
+
const signatures = new Map();
|
|
131
|
+
for (const tenant of registry.tenants) {
|
|
132
|
+
signatures.set(tenant.id, fileSignature(tenant.environmentFile));
|
|
133
|
+
}
|
|
134
|
+
return { registry, signatures };
|
|
135
|
+
};
|
|
136
|
+
const remove = (id) => __awaiter(this, void 0, void 0, function* () {
|
|
137
|
+
var _a;
|
|
138
|
+
const entry = running.get(id);
|
|
139
|
+
if (!entry)
|
|
140
|
+
return;
|
|
141
|
+
running.delete(id);
|
|
142
|
+
log({ level: 'info', message: `Stop tenant ${id} (porta ${entry.port}).` });
|
|
143
|
+
yield stopProcess(entry.process, (_a = options.stopTimeoutMs) !== null && _a !== void 0 ? _a : 5000);
|
|
144
|
+
});
|
|
145
|
+
const startTenant = (tenant, signatures, port) => {
|
|
146
|
+
var _a, _b, _c;
|
|
147
|
+
const assignedPort = (_a = port !== null && port !== void 0 ? port : tenant.port) !== null && _a !== void 0 ? _a : nextPort();
|
|
148
|
+
if ([...running.values()].some((entry) => entry.port === assignedPort)) {
|
|
149
|
+
log({ level: 'error', message: `Porta ${assignedPort} gia in uso: tenant ${tenant.id} non avviato.` });
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const entry = spawnTenant(tenant, assignedPort, (_b = signatures.get(tenant.id)) !== null && _b !== void 0 ? _b : '');
|
|
153
|
+
if (!entry)
|
|
154
|
+
return false;
|
|
155
|
+
running.set(tenant.id, entry);
|
|
156
|
+
log({
|
|
157
|
+
level: 'info',
|
|
158
|
+
message: `Avvio tenant ${tenant.id} sulla porta ${assignedPort} (pid ${(_c = entry.process.pid) !== null && _c !== void 0 ? _c : '?'}).`,
|
|
159
|
+
});
|
|
160
|
+
return true;
|
|
161
|
+
};
|
|
162
|
+
const apply = (registry, signatures) => __awaiter(this, void 0, void 0, function* () {
|
|
163
|
+
var _a;
|
|
164
|
+
const desired = new Map(registry.tenants.filter((tenant) => tenant.enabled).map((tenant) => [tenant.id, tenant]));
|
|
165
|
+
const result = { added: [], removed: [], restarted: [], unchanged: [] };
|
|
166
|
+
for (const id of [...running.keys()]) {
|
|
167
|
+
if (!desired.has(id)) {
|
|
168
|
+
yield remove(id);
|
|
169
|
+
result.removed.push(id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const [id, tenant] of desired) {
|
|
173
|
+
const current = running.get(id);
|
|
174
|
+
if (!current) {
|
|
175
|
+
if (startTenant(tenant, signatures))
|
|
176
|
+
result.added.push(id);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const signature = (_a = signatures.get(id)) !== null && _a !== void 0 ? _a : '';
|
|
180
|
+
if (signature === current.signature) {
|
|
181
|
+
result.unchanged.push(id);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Riavvio sulla stessa porta: il proxy non cambia configurazione.
|
|
185
|
+
const port = current.port;
|
|
186
|
+
yield remove(id);
|
|
187
|
+
if (startTenant(tenant, signatures, port))
|
|
188
|
+
result.restarted.push(id);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
});
|
|
192
|
+
const unwatch = () => {
|
|
193
|
+
if (watchTimer)
|
|
194
|
+
clearInterval(watchTimer);
|
|
195
|
+
if (pendingReload)
|
|
196
|
+
clearTimeout(pendingReload);
|
|
197
|
+
watchTimer = undefined;
|
|
198
|
+
pendingReload = undefined;
|
|
199
|
+
};
|
|
200
|
+
const reload = () => __awaiter(this, void 0, void 0, function* () {
|
|
201
|
+
if (reloading || stopping)
|
|
202
|
+
return { added: [], removed: [], restarted: [], unchanged: [] };
|
|
203
|
+
reloading = true;
|
|
204
|
+
try {
|
|
205
|
+
const signature = accessiTenantDirectorySignature(options.directory);
|
|
206
|
+
let loaded;
|
|
207
|
+
try {
|
|
208
|
+
loaded = load();
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
log({
|
|
212
|
+
level: 'error',
|
|
213
|
+
message: `Reload annullato, configurazione non valida: ${error instanceof Error ? error.message : String(error)}`,
|
|
214
|
+
});
|
|
215
|
+
lastSignature = signature;
|
|
216
|
+
return { added: [], removed: [], restarted: [], unchanged: [] };
|
|
217
|
+
}
|
|
218
|
+
const result = yield apply(loaded.registry, loaded.signatures);
|
|
219
|
+
lastSignature = signature;
|
|
220
|
+
log({
|
|
221
|
+
level: 'info',
|
|
222
|
+
message: `Reload tenant: +${result.added.length} -${result.removed.length} ~${result.restarted.length} =${result.unchanged.length}.`,
|
|
223
|
+
});
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
reloading = false;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
const stop = () => __awaiter(this, void 0, void 0, function* () {
|
|
231
|
+
if (stopping)
|
|
232
|
+
return;
|
|
233
|
+
stopping = true;
|
|
234
|
+
unwatch();
|
|
235
|
+
yield Promise.all([...running.keys()].map((id) => remove(id)));
|
|
236
|
+
});
|
|
237
|
+
const watch = () => {
|
|
238
|
+
var _a, _b;
|
|
239
|
+
if (watchTimer)
|
|
240
|
+
return;
|
|
241
|
+
const interval = Math.max(100, (_a = options.pollIntervalMs) !== null && _a !== void 0 ? _a : 1000);
|
|
242
|
+
const debounce = Math.max(0, (_b = options.debounceMs) !== null && _b !== void 0 ? _b : 200);
|
|
243
|
+
watchTimer = setInterval(() => {
|
|
244
|
+
if (stopping || reloading)
|
|
245
|
+
return;
|
|
246
|
+
const signature = accessiTenantDirectorySignature(options.directory);
|
|
247
|
+
if (signature === lastSignature)
|
|
248
|
+
return;
|
|
249
|
+
if (pendingReload)
|
|
250
|
+
clearTimeout(pendingReload);
|
|
251
|
+
pendingReload = setTimeout(() => {
|
|
252
|
+
pendingReload = undefined;
|
|
253
|
+
void reload();
|
|
254
|
+
}, debounce);
|
|
255
|
+
}, interval);
|
|
256
|
+
if (typeof watchTimer.unref === 'function')
|
|
257
|
+
watchTimer.unref();
|
|
258
|
+
log({ level: 'info', message: `Watch attivo su ${options.directory} (${interval} ms).` });
|
|
259
|
+
};
|
|
260
|
+
return {
|
|
261
|
+
start() {
|
|
262
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
263
|
+
const { registry, signatures } = load();
|
|
264
|
+
lastSignature = accessiTenantDirectorySignature(options.directory);
|
|
265
|
+
yield apply(registry, signatures);
|
|
266
|
+
for (const tenant of registry.tenants.filter((entry) => !entry.enabled)) {
|
|
267
|
+
log({ level: 'info', message: `Tenant ${tenant.id} disabilitato: non avviato.` });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
reload,
|
|
272
|
+
stop,
|
|
273
|
+
state() {
|
|
274
|
+
return [...running.values()].map((entry) => ({
|
|
275
|
+
id: entry.tenant.id,
|
|
276
|
+
port: entry.port,
|
|
277
|
+
file: entry.tenant.file,
|
|
278
|
+
signature: entry.signature,
|
|
279
|
+
}));
|
|
280
|
+
},
|
|
281
|
+
watch,
|
|
282
|
+
unwatch,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** Attende che una condizione diventi vera (utile nei test e nei runbook). */
|
|
286
|
+
function waitForTenantState(supervisor_1, predicate_1) {
|
|
287
|
+
return __awaiter(this, arguments, void 0, function* (supervisor, predicate, timeoutMs = 5000) {
|
|
288
|
+
const deadline = Date.now() + timeoutMs;
|
|
289
|
+
while (!predicate(supervisor.state())) {
|
|
290
|
+
if (Date.now() > deadline)
|
|
291
|
+
throw new Error('timeout in attesa dello stato tenant');
|
|
292
|
+
yield delay(25);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=tenantSupervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenantSupervisor.js","sourceRoot":"","sources":["../../../src/accessi/server/tenantSupervisor.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'crypto';\nimport { readdirSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport type { AccessiTenant, AccessiTenantRegistry } from './tenants';\nimport { loadAccessiTenants } from './tenants';\n\n/** Handle minimo di un processo figlio (compatibile con `child_process.ChildProcess`). */\nexport interface AccessiTenantProcessHandle {\n readonly pid?: number;\n kill(signal?: NodeJS.Signals | number): boolean;\n once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;\n}\n\nexport type AccessiTenantSpawn = (tenant: AccessiTenant, port: number) => AccessiTenantProcessHandle;\n\nexport interface AccessiTenantSupervisorLoggerEvent {\n level: 'info' | 'warn' | 'error';\n message: string;\n}\n\nexport interface AccessiTenantSupervisorOptions {\n directory: string;\n basePort: number;\n spawn: AccessiTenantSpawn;\n allowHeader?: boolean;\n /** Intervallo di polling della cartella quando `watch()` e attivo. Default 1000 ms. */\n pollIntervalMs?: number;\n /** Attesa prima di applicare un reload dopo una modifica. Default 200 ms. */\n debounceMs?: number;\n /** Timeout per SIGTERM prima di SIGKILL. Default 5000 ms. */\n stopTimeoutMs?: number;\n logger?: (event: AccessiTenantSupervisorLoggerEvent) => void;\n}\n\nexport interface AccessiTenantSupervisorState {\n id: string;\n port: number;\n file: string;\n signature: string;\n}\n\nexport interface AccessiTenantReloadResult {\n added: string[];\n removed: string[];\n restarted: string[];\n unchanged: string[];\n}\n\nexport interface AccessiTenantSupervisor {\n start(): Promise<void>;\n reload(): Promise<AccessiTenantReloadResult>;\n stop(): Promise<void>;\n state(): AccessiTenantSupervisorState[];\n watch(): void;\n unwatch(): void;\n}\n\ninterface RunningTenant {\n tenant: AccessiTenant;\n port: number;\n signature: string;\n process: AccessiTenantProcessHandle;\n}\n\nconst ENVIRONMENT_FILE = /\\.environment\\.(ts|mts|cts|js|mjs|cjs)$/;\n\nfunction fileSignature(file: string): string {\n try {\n return createHash('sha256').update(readFileSync(file)).digest('hex');\n } catch {\n return 'unreadable';\n }\n}\n\n/** Firma della cartella: file + contenuto, ordinata per nome. */\nexport function accessiTenantDirectorySignature(directory: string): string {\n let files: string[];\n try {\n files = readdirSync(directory, { withFileTypes: true })\n .filter((entry) => entry.isFile() && ENVIRONMENT_FILE.test(entry.name))\n .map((entry) => entry.name)\n .sort();\n } catch (error) {\n return `error:${error instanceof Error ? error.message : String(error)}`;\n }\n return createHash('sha256')\n .update(files.map((file) => `${file}:${fileSignature(join(directory, file))}`).join('|'))\n .digest('hex');\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function stopProcess(\n handle: AccessiTenantProcessHandle,\n timeoutMs: number,\n): Promise<void> {\n await new Promise<void>((resolve) => {\n let settled = false;\n const done = () => {\n if (settled) return;\n settled = true;\n resolve();\n };\n handle.once('exit', done);\n try {\n if (!handle.kill('SIGTERM')) done();\n } catch {\n done();\n }\n const timer = setTimeout(() => {\n if (settled) return;\n try {\n handle.kill('SIGKILL');\n } catch {\n // Il processo potrebbe essere gia terminato.\n }\n done();\n }, Math.max(50, timeoutMs));\n if (typeof timer.unref === 'function') timer.unref();\n });\n}\n\n/**\n * Supervisore multi-tenant: avvia un processo per tenant e, con `watch()`,\n * riapplica la configurazione quando i file `*.environment.ts` cambiano.\n *\n * Regole di reload:\n * - un file invalido non tocca i tenant attivi;\n * - i tenant invariati non vengono riavviati;\n * - aggiunte/rimozioni/modifiche producono spawn/stop/restart;\n * - la porta di un tenant esistente non cambia durante un restart.\n */\nexport function createAccessiTenantSupervisor(\n options: AccessiTenantSupervisorOptions,\n): AccessiTenantSupervisor {\n if (!options?.directory) throw new Error('createAccessiTenantSupervisor: directory obbligatoria.');\n if (!Number.isInteger(options.basePort) || options.basePort < 1) {\n throw new Error('createAccessiTenantSupervisor: basePort non valida.');\n }\n if (typeof options.spawn !== 'function') {\n throw new Error('createAccessiTenantSupervisor: spawn obbligatorio.');\n }\n\n const log = options.logger ?? (() => undefined);\n const running = new Map<string, RunningTenant>();\n let lastSignature = '';\n let watchTimer: NodeJS.Timeout | undefined;\n let reloading = false;\n let stopping = false;\n let pendingReload: NodeJS.Timeout | undefined;\n\n const nextPort = (): number => {\n const used = new Set([...running.values()].map((entry) => entry.port));\n let port = options.basePort;\n while (used.has(port)) port += 1;\n return port;\n };\n\n const spawnTenant = (tenant: AccessiTenant, port: number, signature: string): RunningTenant | undefined => {\n try {\n const process = options.spawn(tenant, port);\n return { tenant, port, signature, process };\n } catch (error) {\n log({\n level: 'error',\n message: `Avvio tenant ${tenant.id} fallito: ${error instanceof Error ? error.message : String(error)}`,\n });\n return undefined;\n }\n };\n\n const load = (): { registry: AccessiTenantRegistry; signatures: Map<string, string> } => {\n const registry = loadAccessiTenants({\n directory: options.directory,\n ...(options.allowHeader !== undefined ? { allowHeader: options.allowHeader } : {}),\n });\n const signatures = new Map<string, string>();\n for (const tenant of registry.tenants) {\n signatures.set(tenant.id, fileSignature(tenant.environmentFile));\n }\n return { registry, signatures };\n };\n\n const remove = async (id: string): Promise<void> => {\n const entry = running.get(id);\n if (!entry) return;\n running.delete(id);\n log({ level: 'info', message: `Stop tenant ${id} (porta ${entry.port}).` });\n await stopProcess(entry.process, options.stopTimeoutMs ?? 5000);\n };\n\n const startTenant = (tenant: AccessiTenant, signatures: Map<string, string>, port?: number): boolean => {\n const assignedPort = port ?? tenant.port ?? nextPort();\n if ([...running.values()].some((entry) => entry.port === assignedPort)) {\n log({ level: 'error', message: `Porta ${assignedPort} gia in uso: tenant ${tenant.id} non avviato.` });\n return false;\n }\n const entry = spawnTenant(tenant, assignedPort, signatures.get(tenant.id) ?? '');\n if (!entry) return false;\n running.set(tenant.id, entry);\n log({\n level: 'info',\n message: `Avvio tenant ${tenant.id} sulla porta ${assignedPort} (pid ${entry.process.pid ?? '?'}).`,\n });\n return true;\n };\n\n const apply = async (registry: AccessiTenantRegistry, signatures: Map<string, string>): Promise<AccessiTenantReloadResult> => {\n const desired = new Map(registry.tenants.filter((tenant) => tenant.enabled).map((tenant) => [tenant.id, tenant]));\n const result: AccessiTenantReloadResult = { added: [], removed: [], restarted: [], unchanged: [] };\n\n for (const id of [...running.keys()]) {\n if (!desired.has(id)) {\n await remove(id);\n result.removed.push(id);\n }\n }\n\n for (const [id, tenant] of desired) {\n const current = running.get(id);\n if (!current) {\n if (startTenant(tenant, signatures)) result.added.push(id);\n continue;\n }\n const signature = signatures.get(id) ?? '';\n if (signature === current.signature) {\n result.unchanged.push(id);\n continue;\n }\n\n // Riavvio sulla stessa porta: il proxy non cambia configurazione.\n const port = current.port;\n await remove(id);\n if (startTenant(tenant, signatures, port)) result.restarted.push(id);\n }\n\n return result;\n };\n\n const unwatch = (): void => {\n if (watchTimer) clearInterval(watchTimer);\n if (pendingReload) clearTimeout(pendingReload);\n watchTimer = undefined;\n pendingReload = undefined;\n };\n\n const reload = async (): Promise<AccessiTenantReloadResult> => {\n if (reloading || stopping) return { added: [], removed: [], restarted: [], unchanged: [] };\n reloading = true;\n try {\n const signature = accessiTenantDirectorySignature(options.directory);\n let loaded;\n try {\n loaded = load();\n } catch (error) {\n log({\n level: 'error',\n message: `Reload annullato, configurazione non valida: ${error instanceof Error ? error.message : String(error)}`,\n });\n lastSignature = signature;\n return { added: [], removed: [], restarted: [], unchanged: [] };\n }\n const result = await apply(loaded.registry, loaded.signatures);\n lastSignature = signature;\n log({\n level: 'info',\n message: `Reload tenant: +${result.added.length} -${result.removed.length} ~${result.restarted.length} =${result.unchanged.length}.`,\n });\n return result;\n } finally {\n reloading = false;\n }\n };\n\n const stop = async (): Promise<void> => {\n if (stopping) return;\n stopping = true;\n unwatch();\n await Promise.all([...running.keys()].map((id) => remove(id)));\n };\n\n const watch = (): void => {\n if (watchTimer) return;\n const interval = Math.max(100, options.pollIntervalMs ?? 1000);\n const debounce = Math.max(0, options.debounceMs ?? 200);\n watchTimer = setInterval(() => {\n if (stopping || reloading) return;\n const signature = accessiTenantDirectorySignature(options.directory);\n if (signature === lastSignature) return;\n if (pendingReload) clearTimeout(pendingReload);\n pendingReload = setTimeout(() => {\n pendingReload = undefined;\n void reload();\n }, debounce);\n }, interval);\n if (typeof watchTimer.unref === 'function') watchTimer.unref();\n log({ level: 'info', message: `Watch attivo su ${options.directory} (${interval} ms).` });\n };\n\n return {\n async start() {\n const { registry, signatures } = load();\n lastSignature = accessiTenantDirectorySignature(options.directory);\n await apply(registry, signatures);\n for (const tenant of registry.tenants.filter((entry) => !entry.enabled)) {\n log({ level: 'info', message: `Tenant ${tenant.id} disabilitato: non avviato.` });\n }\n },\n reload,\n stop,\n state() {\n return [...running.values()].map((entry) => ({\n id: entry.tenant.id,\n port: entry.port,\n file: entry.tenant.file,\n signature: entry.signature,\n }));\n },\n watch,\n unwatch,\n };\n}\n\n/** Attende che una condizione diventi vera (utile nei test e nei runbook). */\nexport async function waitForTenantState(\n supervisor: AccessiTenantSupervisor,\n predicate: (state: AccessiTenantSupervisorState[]) => boolean,\n timeoutMs = 5000,\n): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n while (!predicate(supervisor.state())) {\n if (Date.now() > deadline) throw new Error('timeout in attesa dello stato tenant');\n await delay(25);\n }\n}\n"]}
|
|
@@ -249,6 +249,7 @@ function loadAccessiTenants(options) {
|
|
|
249
249
|
const pathsSeen = new Map();
|
|
250
250
|
const jwtSecrets = new Map();
|
|
251
251
|
const issuerAudience = new Map();
|
|
252
|
+
const portsSeen = new Map();
|
|
252
253
|
for (const tenant of tenants) {
|
|
253
254
|
if (byId.has(tenant.id)) {
|
|
254
255
|
issues.push(`Tenant duplicato "${tenant.id}" (${tenant.file} e ${(_a = byId.get(tenant.id)) === null || _a === void 0 ? void 0 : _a.file}).`);
|
|
@@ -270,6 +271,13 @@ function loadAccessiTenants(options) {
|
|
|
270
271
|
else
|
|
271
272
|
pathsSeen.set(tenant.path, tenant.id);
|
|
272
273
|
}
|
|
274
|
+
if (tenant.port !== undefined) {
|
|
275
|
+
const existing = portsSeen.get(tenant.port);
|
|
276
|
+
if (existing)
|
|
277
|
+
issues.push(`Porta ${tenant.port} assegnata a "${existing}" e "${tenant.id}".`);
|
|
278
|
+
else
|
|
279
|
+
portsSeen.set(tenant.port, tenant.id);
|
|
280
|
+
}
|
|
273
281
|
// Un segreto JWT condiviso rende un token valido su piu tenant.
|
|
274
282
|
const signingKey = (_b = tenant.options.jwtOptions.privateKey) !== null && _b !== void 0 ? _b : tenant.options.jwtOptions.secret;
|
|
275
283
|
const fingerprintKey = `${(_c = tenant.options.jwtOptions.algorithm) !== null && _c !== void 0 ? _c : 'HS256'}|${signingKey}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenants.js","sourceRoot":"","sources":["../../../src/accessi/server/tenants.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync } from 'fs';\nimport { basename, join, resolve } from 'path';\nimport { WIRE_CRYPT_DISABLE, WIRE_CRYPT_ENABLE } from 'node-firebird';\nimport type { Options } from 'node-firebird';\nimport type {\n AccessiOptions,\n AccessiHttpOptions,\n AdminBootstrapOptions,\n CatalogScriptsOptions,\n ConsoleEntryOptions,\n EmailOptions,\n ExtensionFieldsOptions,\n FederatedAuthenticationOptions,\n JwtOptions,\n PublicAuthRateLimitOptions,\n PublicRegistrationOptions,\n ServiceTokenOptions,\n SystemSeedsOptions,\n} from '../../accessi-module/AccessiModule';\nimport type { AccessiEdgeRateLimitOptions } from '../../accessi-module/security/edgeRateLimit';\nimport type { AccessiAuthCookieOptions } from '../../accessi-module/security/authCookies';\nimport type { AccessiMetricsOptions } from '../../accessi-module/observability/metrics';\nimport type { AccessiAuditOptions } from '../../accessi-module/security/audit';\nimport type { SessionRevocationOptions } from '../../accessi-module/security/sessionRevocation';\nimport type { AccessTokenRevocationOptions } from '../../accessi-module/security/accessTokenRevocation';\nimport type { RefreshTokenRotationOptions } from '../../accessi-module/security/refreshTokenRotation';\nimport type { OpenApiSetupOptions } from '../../openapi/OpenApiConfig';\nimport { validateAccessiOptions } from '../../accessi-module/security/validateOptions';\n\n/** Identita e routing del tenant. */\nexport interface AccessiTenantSettings {\n /** Minuscole, numeri, `-` e `_`; max 63 caratteri. */\n id: string;\n /** Host esatti o wildcard `*.example.com`. */\n hosts?: string[];\n /** Prefisso path, es. `/acme`. */\n path?: string;\n /** Porta del processo quando si usa il supervisore. */\n port?: number;\n /** Default `true`. */\n enabled?: boolean;\n description?: string;\n}\n\n/** Connessione Firebird del tenant, tipizzata senza callback. */\nexport interface AccessiTenantDatabaseOptions {\n host: string;\n port?: number;\n database: string;\n user?: string;\n password?: string;\n /** `disabled`/`optional`; `required` e il default del driver. */\n wireCrypt?: 'required' | 'optional' | 'disabled';\n pluginName?: string;\n}\n\n/**\n * Configurazione serializzabile di un tenant.\n *\n * I campi che contengono store, callback o logger (`store`, `sink`, `match`,\n * `onDocument`, `logger`, ...) sono esclusi: vanno collegati dal backend\n * ospitante, non da un file di configurazione.\n */\nexport interface AccessiTenantEnvironment {\n tenant: AccessiTenantSettings;\n database: AccessiTenantDatabaseOptions;\n jwt: JwtOptions;\n email: EmailOptions;\n encryptionKey: string;\n confirmationEmailUrl: string;\n confirmationEmailReturnUrl: string;\n confirmationEmailPrefix?: string;\n customResetPage?: string;\n publicBaseUrl?: string;\n consoleApiBaseUrl?: string;\n mockDemoUser?: boolean;\n passwordExpiration?: boolean;\n passwordExpirationDays?: number;\n legacyPasswordMigrationOnStartup?: boolean;\n autoUpdateDatabase?: boolean;\n http?: AccessiHttpOptions;\n authCookie?: AccessiAuthCookieOptions;\n publicAuthRateLimit?: Omit<PublicAuthRateLimitOptions, 'store'>;\n publicRegistration?: PublicRegistrationOptions;\n federatedAuthentication?: FederatedAuthenticationOptions;\n extensionFieldsOptions?: ExtensionFieldsOptions[];\n serviceTokens?: Omit<ServiceTokenOptions, 'rateLimitStore'>;\n catalogScripts?: CatalogScriptsOptions;\n systemSeeds?: SystemSeedsOptions;\n adminBootstrap?: AdminBootstrapOptions;\n openapi?: Omit<OpenApiSetupOptions, 'onDocument'>;\n consoleEntry?: Omit<ConsoleEntryOptions, 'store'>;\n edgeRateLimit?: Omit<AccessiEdgeRateLimitOptions, 'store' | 'match'>;\n sessionRevocation?: Omit<SessionRevocationOptions, 'store'>;\n accessTokenRevocation?: Omit<AccessTokenRevocationOptions, 'store'>;\n refreshTokenRotation?: Omit<RefreshTokenRotationOptions, 'store'>;\n metrics?: Omit<AccessiMetricsOptions, 'sink'>;\n audit?: Omit<AccessiAuditOptions, 'sinks' | 'store' | 'tenantId'>;\n bufferLogs?: boolean;\n}\n\nexport interface AccessiTenant {\n id: string;\n hosts: string[];\n path?: string;\n enabled: boolean;\n description?: string;\n port?: number;\n /** Nome del file `*.environment.ts` di origine (mai il contenuto). */\n file: string;\n environmentFile: string;\n options: AccessiOptions;\n}\n\nexport interface AccessiTenantSummary {\n id: string;\n hosts: string[];\n path?: string;\n enabled: boolean;\n description?: string;\n port?: number;\n file: string;\n database: {\n host?: string;\n port?: number;\n database?: string;\n user?: string;\n };\n issuer?: string;\n audience?: string;\n jwtAlgorithm: string;\n refreshEnabled: boolean;\n}\n\nexport interface AccessiTenantResolution {\n tenant: AccessiTenant;\n matchedBy: 'host' | 'path' | 'header';\n}\n\nexport interface AccessiTenantRegistry {\n directory: string;\n tenants: AccessiTenant[];\n warnings: string[];\n allowHeader: boolean;\n byId: ReadonlyMap<string, AccessiTenant>;\n resolve(input: { host?: string; path?: string; header?: string }, options?: { allowHeader?: boolean }): AccessiTenantResolution | undefined;\n summary(): AccessiTenantSummary[];\n}\n\nexport interface LoadAccessiTenantsOptions {\n /** Cartella con `<id>.environment.ts`. */\n directory: string;\n /** Abilita la risoluzione via header `x-accessi-tenant`. Default: false. */\n allowHeader?: boolean;\n /** Consente issuer/audience uguali tra tenant (sconsigliato). Default: false. */\n allowSharedIssuer?: boolean;\n}\n\n/** Errore aggregato di configurazione tenant. */\nexport class AccessiTenantConfigurationError extends Error {\n readonly issues: string[];\n\n constructor(issues: string[]) {\n super(`Configurazione tenant non valida:\\n${issues.map((issue) => `- ${issue}`).join('\\n')}`);\n this.name = 'AccessiTenantConfigurationError';\n this.issues = issues;\n }\n}\n\n/**\n * Helper di tipizzazione per i file tenant.\n *\n * ```ts\n * import type { AccessiTenantEnvironment } from 'emilsoftware-utilities/accessi/server';\n * import { defineAccessiTenantEnvironment } from 'emilsoftware-utilities/accessi/server';\n *\n * export default defineAccessiTenantEnvironment({\n * tenant: { id: 'acme', hosts: ['accessi.acme.example.com'] },\n * database: { host: 'db-acme', database: '/data/acme.fdb' },\n * jwt: { secret: process.env.ACME_JWT_SECRET!, expiresIn: '1h' },\n * email: { ... },\n * encryptionKey: process.env.ACME_ENCRYPTION_KEY!,\n * confirmationEmailUrl: 'https://accessi.acme.example.com',\n * confirmationEmailReturnUrl: 'https://accessi.acme.example.com',\n * });\n * ```\n */\nexport function defineAccessiTenantEnvironment<const T extends AccessiTenantEnvironment>(\n environment: T,\n): T {\n return environment;\n}\n\nfunction normalizeHost(host: string): string {\n return host.trim().toLowerCase().replace(/\\.$/, '');\n}\n\nfunction normalizeTenantPath(path: string | undefined): string | undefined {\n if (path === undefined || path.trim() === '') return undefined;\n const normalized = `/${path.trim().replace(/^\\/+|\\/+$/g, '')}`;\n return normalized === '/' ? '/' : normalized;\n}\n\nfunction matchesHost(tenant: AccessiTenant, host: string): boolean {\n return tenant.hosts.some((candidate) => {\n if (candidate === host) return true;\n if (candidate.startsWith('*.')) {\n const suffix = candidate.slice(1);\n return host.endsWith(suffix) && host.length > suffix.length;\n }\n return false;\n });\n}\n\nfunction matchesPath(tenant: AccessiTenant, path: string): boolean {\n if (!tenant.path) return false;\n if (tenant.path === '/') return true;\n return path === tenant.path || path.startsWith(`${tenant.path}/`);\n}\n\n/** Prefisso Redis consigliato per isolare gli store del tenant. */\nexport function getAccessiTenantRedisPrefix(tenantId: string): string {\n return `accessi:${tenantId}:`;\n}\n\n/**\n * Converte la configurazione tipizzata in `AccessiOptions`.\n *\n * Applica i default di Firebird e collega `tenantId`; non crea store o sink,\n * che restano responsabilita del backend ospitante.\n */\nexport function accessiOptionsFromTenantEnvironment(\n environment: AccessiTenantEnvironment,\n): AccessiOptions {\n const database = environment.database;\n const databaseOptions: Options = {\n host: database.host,\n port: database.port ?? 3050,\n database: database.database,\n user: database.user ?? 'SYSDBA',\n password: database.password ?? 'masterkey',\n ...(database.pluginName ? { pluginName: database.pluginName } : {}),\n ...(database.wireCrypt === 'disabled'\n ? { wireCrypt: WIRE_CRYPT_DISABLE }\n : database.wireCrypt === 'optional'\n ? { wireCrypt: WIRE_CRYPT_ENABLE }\n : {}),\n };\n\n return {\n databaseOptions,\n confirmationEmailUrl: environment.confirmationEmailUrl,\n confirmationEmailReturnUrl: environment.confirmationEmailReturnUrl,\n ...(environment.confirmationEmailPrefix ? { confirmationEmailPrefix: environment.confirmationEmailPrefix } : {}),\n ...(environment.customResetPage ? { customResetPage: environment.customResetPage } : {}),\n ...(environment.publicBaseUrl ? { publicBaseUrl: environment.publicBaseUrl } : {}),\n ...(environment.consoleApiBaseUrl ? { consoleApiBaseUrl: environment.consoleApiBaseUrl } : {}),\n encryptionKey: environment.encryptionKey,\n mockDemoUser: environment.mockDemoUser ?? false,\n ...(environment.passwordExpiration !== undefined ? { passwordExpiration: environment.passwordExpiration } : {}),\n ...(environment.passwordExpirationDays !== undefined ? { passwordExpirationDays: environment.passwordExpirationDays } : {}),\n ...(environment.legacyPasswordMigrationOnStartup !== undefined\n ? { legacyPasswordMigrationOnStartup: environment.legacyPasswordMigrationOnStartup }\n : {}),\n ...(environment.autoUpdateDatabase !== undefined ? { autoUpdateDatabase: environment.autoUpdateDatabase } : {}),\n jwtOptions: environment.jwt,\n emailOptions: environment.email,\n ...(environment.http ? { http: environment.http } : {}),\n ...(environment.authCookie ? { authCookie: environment.authCookie } : {}),\n ...(environment.publicAuthRateLimit ? { publicAuthRateLimit: environment.publicAuthRateLimit } : {}),\n ...(environment.publicRegistration ? { publicRegistration: environment.publicRegistration } : {}),\n ...(environment.federatedAuthentication ? { federatedAuthentication: environment.federatedAuthentication } : {}),\n ...(environment.extensionFieldsOptions ? { extensionFieldsOptions: environment.extensionFieldsOptions } : {}),\n ...(environment.serviceTokens ? { serviceTokens: environment.serviceTokens } : {}),\n ...(environment.catalogScripts ? { catalogScripts: environment.catalogScripts } : {}),\n ...(environment.systemSeeds ? { systemSeeds: environment.systemSeeds } : {}),\n ...(environment.adminBootstrap ? { adminBootstrap: environment.adminBootstrap } : {}),\n ...(environment.openapi ? { openapi: environment.openapi } : {}),\n ...(environment.consoleEntry ? { consoleEntry: environment.consoleEntry } : {}),\n ...(environment.edgeRateLimit ? { edgeRateLimit: environment.edgeRateLimit } : {}),\n ...(environment.sessionRevocation ? { sessionRevocation: environment.sessionRevocation } : {}),\n ...(environment.accessTokenRevocation ? { accessTokenRevocation: environment.accessTokenRevocation } : {}),\n ...(environment.refreshTokenRotation ? { refreshTokenRotation: environment.refreshTokenRotation } : {}),\n ...(environment.metrics ? { metrics: environment.metrics } : {}),\n ...(environment.audit ? { audit: environment.audit } : {}),\n ...(environment.bufferLogs !== undefined ? { bufferLogs: environment.bufferLogs } : {}),\n tenantId: environment.tenant.id,\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction runtimeIssues(environment: unknown, file: string): string[] {\n const issues: string[] = [];\n if (!isRecord(environment)) {\n return [`${file}: il file deve esportare un oggetto (default export o export environment).`];\n }\n const tenant = environment.tenant;\n if (!isRecord(tenant)) {\n issues.push(`${file}: tenant mancante.`);\n } else {\n if (typeof tenant.id !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(tenant.id)) {\n issues.push(`${file}: tenant.id non valido (minuscole, numeri, - e _, max 63).`);\n }\n if (tenant.hosts !== undefined && (!Array.isArray(tenant.hosts) || tenant.hosts.some((host) => typeof host !== 'string'))) {\n issues.push(`${file}: tenant.hosts deve essere un array di stringhe.`);\n }\n if (tenant.path !== undefined && typeof tenant.path !== 'string') {\n issues.push(`${file}: tenant.path deve essere una stringa.`);\n }\n if (\n tenant.port !== undefined\n && (typeof tenant.port !== 'number' || !Number.isInteger(tenant.port) || tenant.port < 1 || tenant.port > 65535)\n ) {\n issues.push(`${file}: tenant.port non valida.`);\n }\n }\n\n const database = environment.database;\n if (!isRecord(database)) {\n issues.push(`${file}: database mancante.`);\n } else {\n if (typeof database.host !== 'string' || database.host.trim() === '') {\n issues.push(`${file}: database.host obbligatorio.`);\n }\n if (typeof database.database !== 'string' || database.database.trim() === '') {\n issues.push(`${file}: database.database obbligatorio.`);\n }\n if (\n database.port !== undefined\n && (typeof database.port !== 'number' || !Number.isInteger(database.port) || database.port < 1 || database.port > 65535)\n ) {\n issues.push(`${file}: database.port non valida.`);\n }\n }\n\n if (!isRecord(environment.jwt)) issues.push(`${file}: jwt mancante.`);\n else {\n if (typeof environment.jwt.secret !== 'string' || environment.jwt.secret === '') {\n issues.push(`${file}: jwt.secret obbligatorio.`);\n }\n if (typeof environment.jwt.expiresIn !== 'string' || environment.jwt.expiresIn === '') {\n issues.push(`${file}: jwt.expiresIn obbligatorio.`);\n }\n }\n\n if (!isRecord(environment.email)) issues.push(`${file}: email mancante.`);\n if (typeof environment.encryptionKey !== 'string' || environment.encryptionKey === '') {\n issues.push(`${file}: encryptionKey obbligatoria.`);\n }\n if (typeof environment.confirmationEmailUrl !== 'string') {\n issues.push(`${file}: confirmationEmailUrl obbligatoria.`);\n }\n if (typeof environment.confirmationEmailReturnUrl !== 'string') {\n issues.push(`${file}: confirmationEmailReturnUrl obbligatoria.`);\n }\n return issues;\n}\n\nfunction loadEnvironmentModule(file: string): unknown {\n if (/\\.(ts|mts|cts)$/i.test(file)) {\n // Caricamento lazy: jiti serve solo ai file tenant TypeScript.\n const { createJiti } = require('jiti') as {\n createJiti: (\n id: string,\n options?: { interopDefault?: boolean; moduleCache?: boolean; fsCache?: boolean },\n ) => (id: string) => unknown;\n };\n const jiti = createJiti(file, { interopDefault: true, moduleCache: false, fsCache: false });\n return jiti(file);\n }\n return require(file);\n}\n\n/**\n * Carica un singolo file `*.environment.ts`.\n *\n * Usa jiti per eseguire TypeScript anche nel pacchetto compilato, senza\n * richiedere un passo di build separato per i file tenant.\n */\nexport function loadAccessiTenantEnvironmentFile(file: string): { tenant: AccessiTenant; environment: AccessiTenantEnvironment } {\n const absolute = resolve(file);\n let loaded: unknown;\n try {\n loaded = loadEnvironmentModule(absolute);\n } catch (error) {\n throw new AccessiTenantConfigurationError([\n `${basename(file)}: impossibile caricare il file (${error instanceof Error ? error.message : String(error)}).`,\n ]);\n }\n\n const record = isRecord(loaded) ? loaded : {};\n const environment = (record.default ?? record.environment ?? loaded) as unknown;\n const issues = runtimeIssues(environment, basename(file));\n if (issues.length > 0) throw new AccessiTenantConfigurationError(issues);\n\n const typed = environment as AccessiTenantEnvironment;\n const settings = typed.tenant;\n const options = accessiOptionsFromTenantEnvironment(typed);\n const validation = validateAccessiOptions(options);\n const validationIssues = validation.errors.map((issue) => `${basename(file)}: ${issue.code}: ${issue.message}`);\n if (validationIssues.length > 0) throw new AccessiTenantConfigurationError(validationIssues);\n\n const hosts = (settings.hosts ?? []).map(normalizeHost).filter(Boolean);\n const path = normalizeTenantPath(settings.path);\n if (hosts.length === 0 && !path) {\n throw new AccessiTenantConfigurationError([\n `${basename(file)}: configurare tenant.hosts oppure tenant.path.`,\n ]);\n }\n\n const tenant: AccessiTenant = {\n id: settings.id,\n hosts,\n ...(path ? { path } : {}),\n enabled: settings.enabled !== false,\n ...(settings.description ? { description: settings.description } : {}),\n ...(settings.port ? { port: settings.port } : {}),\n file: basename(file),\n environmentFile: absolute,\n options,\n };\n return { tenant, environment: typed };\n}\n\n/**\n * Carica i tenant da una cartella di file `<id>.environment.ts`.\n *\n * La validazione e aggregata: un errore blocca l'intera cartella, cosi non si\n * avvia un tenant con configurazione parziale. Nessun segreto viene stampato.\n */\nexport function loadAccessiTenants(options: LoadAccessiTenantsOptions): AccessiTenantRegistry {\n if (!options?.directory) throw new Error('loadAccessiTenants: directory obbligatoria.');\n const directory = resolve(options.directory);\n const issues: string[] = [];\n const warnings: string[] = [];\n\n let files: string[];\n try {\n files = readdirSync(directory, { withFileTypes: true })\n .filter((entry) => entry.isFile() && /\\.environment\\.(ts|mts|cts|js|mjs|cjs)$/.test(entry.name))\n .map((entry) => entry.name)\n .sort();\n } catch (error) {\n throw new AccessiTenantConfigurationError([\n `Cartella tenant non leggibile (${directory}): ${error instanceof Error ? error.message : String(error)}`,\n ]);\n }\n if (files.length === 0) {\n throw new AccessiTenantConfigurationError([`Nessun file *.environment.ts nella cartella ${directory}.`]);\n }\n\n const tenants: AccessiTenant[] = [];\n for (const file of files) {\n try {\n tenants.push(loadAccessiTenantEnvironmentFile(join(directory, file)).tenant);\n } catch (error) {\n if (error instanceof AccessiTenantConfigurationError) issues.push(...error.issues);\n else issues.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n const byId = new Map<string, AccessiTenant>();\n const hostsSeen = new Map<string, string>();\n const pathsSeen = new Map<string, string>();\n const jwtSecrets = new Map<string, string>();\n const issuerAudience = new Map<string, string>();\n for (const tenant of tenants) {\n if (byId.has(tenant.id)) {\n issues.push(`Tenant duplicato \"${tenant.id}\" (${tenant.file} e ${byId.get(tenant.id)?.file}).`);\n } else {\n byId.set(tenant.id, tenant);\n }\n for (const host of tenant.hosts) {\n const existing = hostsSeen.get(host);\n if (existing) issues.push(`Host \"${host}\" assegnato a \"${existing}\" e \"${tenant.id}\".`);\n else hostsSeen.set(host, tenant.id);\n }\n if (tenant.path) {\n const existing = pathsSeen.get(tenant.path);\n if (existing) issues.push(`Path \"${tenant.path}\" assegnato a \"${existing}\" e \"${tenant.id}\".`);\n else pathsSeen.set(tenant.path, tenant.id);\n }\n\n // Un segreto JWT condiviso rende un token valido su piu tenant.\n const signingKey = tenant.options.jwtOptions.privateKey ?? tenant.options.jwtOptions.secret;\n const fingerprintKey = `${tenant.options.jwtOptions.algorithm ?? 'HS256'}|${signingKey}`;\n const existingKey = jwtSecrets.get(fingerprintKey);\n if (existingKey) {\n issues.push(`Chiave JWT condivisa tra \"${existingKey}\" e \"${tenant.id}\": usare segreti distinti.`);\n } else {\n jwtSecrets.set(fingerprintKey, tenant.id);\n }\n\n const issuer = tenant.options.jwtOptions.issuer ?? '(assente)';\n const audience = Array.isArray(tenant.options.jwtOptions.audience)\n ? tenant.options.jwtOptions.audience.join(',')\n : (tenant.options.jwtOptions.audience ?? '(assente)');\n const issuerKey = `${issuer}|${audience}`;\n const existingIssuer = issuerAudience.get(issuerKey);\n if (existingIssuer && options.allowSharedIssuer !== true) {\n warnings.push(`Issuer/audience \"${issuerKey}\" condivisi tra \"${existingIssuer}\" e \"${tenant.id}\".`);\n } else {\n issuerAudience.set(issuerKey, tenant.id);\n }\n }\n\n const enabled = tenants.filter((tenant) => tenant.enabled);\n if (enabled.length > 1) {\n const defaultTenant = enabled.find((tenant) => tenant.hosts.length === 0 && tenant.path === '/');\n if (defaultTenant) {\n issues.push(`Tenant \"${defaultTenant.id}\" intercetta tutte le richieste (path \"/\") insieme ad altri tenant.`);\n }\n }\n\n if (issues.length > 0) throw new AccessiTenantConfigurationError(issues);\n\n const registry: AccessiTenantRegistry = {\n directory,\n tenants,\n warnings,\n allowHeader: options.allowHeader === true,\n byId,\n resolve(input, resolveOptions) {\n const allowHeader = resolveOptions?.allowHeader ?? registry.allowHeader;\n const host = input.host ? normalizeHost(input.host.split(':')[0] ?? input.host) : undefined;\n if (host) {\n const byHost = enabled.find((tenant) => matchesHost(tenant, host));\n if (byHost) return { tenant: byHost, matchedBy: 'host' };\n }\n\n const requestPath = input.path ?? '/';\n const byPath = enabled\n .filter((tenant) => matchesPath(tenant, requestPath))\n .sort((left, right) => (right.path?.length ?? 0) - (left.path?.length ?? 0))[0];\n if (byPath && byPath.path !== '/') return { tenant: byPath, matchedBy: 'path' };\n\n if (allowHeader && input.header) {\n const headerId = input.header.trim().toLowerCase();\n const tenant = byId.get(headerId);\n if (tenant?.enabled) return { tenant, matchedBy: 'header' };\n }\n\n if (byPath) return { tenant: byPath, matchedBy: 'path' };\n return undefined;\n },\n summary() {\n return tenants.map((tenant) => {\n const database = tenant.options.databaseOptions as {\n host?: string;\n port?: number;\n database?: string;\n user?: string;\n };\n return {\n id: tenant.id,\n hosts: [...tenant.hosts],\n ...(tenant.path ? { path: tenant.path } : {}),\n enabled: tenant.enabled,\n ...(tenant.description ? { description: tenant.description } : {}),\n ...(tenant.port ? { port: tenant.port } : {}),\n file: tenant.file,\n database: {\n host: database.host,\n port: database.port,\n database: database.database,\n user: database.user,\n },\n issuer: tenant.options.jwtOptions.issuer,\n ...(tenant.options.jwtOptions.audience\n ? {\n audience: typeof tenant.options.jwtOptions.audience === 'string'\n ? tenant.options.jwtOptions.audience\n : tenant.options.jwtOptions.audience.join(','),\n }\n : {}),\n jwtAlgorithm: tenant.options.jwtOptions.algorithm ?? 'HS256',\n refreshEnabled: Boolean(tenant.options.jwtOptions.refreshExpiresIn),\n };\n });\n },\n };\n\n return registry;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tenants.js","sourceRoot":"","sources":["../../../src/accessi/server/tenants.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync } from 'fs';\nimport { basename, join, resolve } from 'path';\nimport { WIRE_CRYPT_DISABLE, WIRE_CRYPT_ENABLE } from 'node-firebird';\nimport type { Options } from 'node-firebird';\nimport type {\n AccessiOptions,\n AccessiHttpOptions,\n AdminBootstrapOptions,\n CatalogScriptsOptions,\n ConsoleEntryOptions,\n EmailOptions,\n ExtensionFieldsOptions,\n FederatedAuthenticationOptions,\n JwtOptions,\n PublicAuthRateLimitOptions,\n PublicRegistrationOptions,\n ServiceTokenOptions,\n SystemSeedsOptions,\n} from '../../accessi-module/AccessiModule';\nimport type { AccessiEdgeRateLimitOptions } from '../../accessi-module/security/edgeRateLimit';\nimport type { AccessiAuthCookieOptions } from '../../accessi-module/security/authCookies';\nimport type { AccessiMetricsOptions } from '../../accessi-module/observability/metrics';\nimport type { AccessiAuditOptions } from '../../accessi-module/security/audit';\nimport type { SessionRevocationOptions } from '../../accessi-module/security/sessionRevocation';\nimport type { AccessTokenRevocationOptions } from '../../accessi-module/security/accessTokenRevocation';\nimport type { RefreshTokenRotationOptions } from '../../accessi-module/security/refreshTokenRotation';\nimport type { OpenApiSetupOptions } from '../../openapi/OpenApiConfig';\nimport { validateAccessiOptions } from '../../accessi-module/security/validateOptions';\n\n/** Identita e routing del tenant. */\nexport interface AccessiTenantSettings {\n /** Minuscole, numeri, `-` e `_`; max 63 caratteri. */\n id: string;\n /** Host esatti o wildcard `*.example.com`. */\n hosts?: string[];\n /** Prefisso path, es. `/acme`. */\n path?: string;\n /** Porta del processo quando si usa il supervisore. */\n port?: number;\n /** Default `true`. */\n enabled?: boolean;\n description?: string;\n}\n\n/** Connessione Firebird del tenant, tipizzata senza callback. */\nexport interface AccessiTenantDatabaseOptions {\n host: string;\n port?: number;\n database: string;\n user?: string;\n password?: string;\n /** `disabled`/`optional`; `required` e il default del driver. */\n wireCrypt?: 'required' | 'optional' | 'disabled';\n pluginName?: string;\n}\n\n/**\n * Configurazione serializzabile di un tenant.\n *\n * I campi che contengono store, callback o logger (`store`, `sink`, `match`,\n * `onDocument`, `logger`, ...) sono esclusi: vanno collegati dal backend\n * ospitante, non da un file di configurazione.\n */\nexport interface AccessiTenantEnvironment {\n tenant: AccessiTenantSettings;\n database: AccessiTenantDatabaseOptions;\n jwt: JwtOptions;\n email: EmailOptions;\n encryptionKey: string;\n confirmationEmailUrl: string;\n confirmationEmailReturnUrl: string;\n confirmationEmailPrefix?: string;\n customResetPage?: string;\n publicBaseUrl?: string;\n consoleApiBaseUrl?: string;\n mockDemoUser?: boolean;\n passwordExpiration?: boolean;\n passwordExpirationDays?: number;\n legacyPasswordMigrationOnStartup?: boolean;\n autoUpdateDatabase?: boolean;\n http?: AccessiHttpOptions;\n authCookie?: AccessiAuthCookieOptions;\n publicAuthRateLimit?: Omit<PublicAuthRateLimitOptions, 'store'>;\n publicRegistration?: PublicRegistrationOptions;\n federatedAuthentication?: FederatedAuthenticationOptions;\n extensionFieldsOptions?: ExtensionFieldsOptions[];\n serviceTokens?: Omit<ServiceTokenOptions, 'rateLimitStore'>;\n catalogScripts?: CatalogScriptsOptions;\n systemSeeds?: SystemSeedsOptions;\n adminBootstrap?: AdminBootstrapOptions;\n openapi?: Omit<OpenApiSetupOptions, 'onDocument'>;\n consoleEntry?: Omit<ConsoleEntryOptions, 'store'>;\n edgeRateLimit?: Omit<AccessiEdgeRateLimitOptions, 'store' | 'match'>;\n sessionRevocation?: Omit<SessionRevocationOptions, 'store'>;\n accessTokenRevocation?: Omit<AccessTokenRevocationOptions, 'store'>;\n refreshTokenRotation?: Omit<RefreshTokenRotationOptions, 'store'>;\n metrics?: Omit<AccessiMetricsOptions, 'sink'>;\n audit?: Omit<AccessiAuditOptions, 'sinks' | 'store' | 'tenantId'>;\n bufferLogs?: boolean;\n}\n\nexport interface AccessiTenant {\n id: string;\n hosts: string[];\n path?: string;\n enabled: boolean;\n description?: string;\n port?: number;\n /** Nome del file `*.environment.ts` di origine (mai il contenuto). */\n file: string;\n environmentFile: string;\n options: AccessiOptions;\n}\n\nexport interface AccessiTenantSummary {\n id: string;\n hosts: string[];\n path?: string;\n enabled: boolean;\n description?: string;\n port?: number;\n file: string;\n database: {\n host?: string;\n port?: number;\n database?: string;\n user?: string;\n };\n issuer?: string;\n audience?: string;\n jwtAlgorithm: string;\n refreshEnabled: boolean;\n}\n\nexport interface AccessiTenantResolution {\n tenant: AccessiTenant;\n matchedBy: 'host' | 'path' | 'header';\n}\n\nexport interface AccessiTenantRegistry {\n directory: string;\n tenants: AccessiTenant[];\n warnings: string[];\n allowHeader: boolean;\n byId: ReadonlyMap<string, AccessiTenant>;\n resolve(input: { host?: string; path?: string; header?: string }, options?: { allowHeader?: boolean }): AccessiTenantResolution | undefined;\n summary(): AccessiTenantSummary[];\n}\n\nexport interface LoadAccessiTenantsOptions {\n /** Cartella con `<id>.environment.ts`. */\n directory: string;\n /** Abilita la risoluzione via header `x-accessi-tenant`. Default: false. */\n allowHeader?: boolean;\n /** Consente issuer/audience uguali tra tenant (sconsigliato). Default: false. */\n allowSharedIssuer?: boolean;\n}\n\n/** Errore aggregato di configurazione tenant. */\nexport class AccessiTenantConfigurationError extends Error {\n readonly issues: string[];\n\n constructor(issues: string[]) {\n super(`Configurazione tenant non valida:\\n${issues.map((issue) => `- ${issue}`).join('\\n')}`);\n this.name = 'AccessiTenantConfigurationError';\n this.issues = issues;\n }\n}\n\n/**\n * Helper di tipizzazione per i file tenant.\n *\n * ```ts\n * import type { AccessiTenantEnvironment } from 'emilsoftware-utilities/accessi/server';\n * import { defineAccessiTenantEnvironment } from 'emilsoftware-utilities/accessi/server';\n *\n * export default defineAccessiTenantEnvironment({\n * tenant: { id: 'acme', hosts: ['accessi.acme.example.com'] },\n * database: { host: 'db-acme', database: '/data/acme.fdb' },\n * jwt: { secret: process.env.ACME_JWT_SECRET!, expiresIn: '1h' },\n * email: { ... },\n * encryptionKey: process.env.ACME_ENCRYPTION_KEY!,\n * confirmationEmailUrl: 'https://accessi.acme.example.com',\n * confirmationEmailReturnUrl: 'https://accessi.acme.example.com',\n * });\n * ```\n */\nexport function defineAccessiTenantEnvironment<const T extends AccessiTenantEnvironment>(\n environment: T,\n): T {\n return environment;\n}\n\nfunction normalizeHost(host: string): string {\n return host.trim().toLowerCase().replace(/\\.$/, '');\n}\n\nfunction normalizeTenantPath(path: string | undefined): string | undefined {\n if (path === undefined || path.trim() === '') return undefined;\n const normalized = `/${path.trim().replace(/^\\/+|\\/+$/g, '')}`;\n return normalized === '/' ? '/' : normalized;\n}\n\nfunction matchesHost(tenant: AccessiTenant, host: string): boolean {\n return tenant.hosts.some((candidate) => {\n if (candidate === host) return true;\n if (candidate.startsWith('*.')) {\n const suffix = candidate.slice(1);\n return host.endsWith(suffix) && host.length > suffix.length;\n }\n return false;\n });\n}\n\nfunction matchesPath(tenant: AccessiTenant, path: string): boolean {\n if (!tenant.path) return false;\n if (tenant.path === '/') return true;\n return path === tenant.path || path.startsWith(`${tenant.path}/`);\n}\n\n/** Prefisso Redis consigliato per isolare gli store del tenant. */\nexport function getAccessiTenantRedisPrefix(tenantId: string): string {\n return `accessi:${tenantId}:`;\n}\n\n/**\n * Converte la configurazione tipizzata in `AccessiOptions`.\n *\n * Applica i default di Firebird e collega `tenantId`; non crea store o sink,\n * che restano responsabilita del backend ospitante.\n */\nexport function accessiOptionsFromTenantEnvironment(\n environment: AccessiTenantEnvironment,\n): AccessiOptions {\n const database = environment.database;\n const databaseOptions: Options = {\n host: database.host,\n port: database.port ?? 3050,\n database: database.database,\n user: database.user ?? 'SYSDBA',\n password: database.password ?? 'masterkey',\n ...(database.pluginName ? { pluginName: database.pluginName } : {}),\n ...(database.wireCrypt === 'disabled'\n ? { wireCrypt: WIRE_CRYPT_DISABLE }\n : database.wireCrypt === 'optional'\n ? { wireCrypt: WIRE_CRYPT_ENABLE }\n : {}),\n };\n\n return {\n databaseOptions,\n confirmationEmailUrl: environment.confirmationEmailUrl,\n confirmationEmailReturnUrl: environment.confirmationEmailReturnUrl,\n ...(environment.confirmationEmailPrefix ? { confirmationEmailPrefix: environment.confirmationEmailPrefix } : {}),\n ...(environment.customResetPage ? { customResetPage: environment.customResetPage } : {}),\n ...(environment.publicBaseUrl ? { publicBaseUrl: environment.publicBaseUrl } : {}),\n ...(environment.consoleApiBaseUrl ? { consoleApiBaseUrl: environment.consoleApiBaseUrl } : {}),\n encryptionKey: environment.encryptionKey,\n mockDemoUser: environment.mockDemoUser ?? false,\n ...(environment.passwordExpiration !== undefined ? { passwordExpiration: environment.passwordExpiration } : {}),\n ...(environment.passwordExpirationDays !== undefined ? { passwordExpirationDays: environment.passwordExpirationDays } : {}),\n ...(environment.legacyPasswordMigrationOnStartup !== undefined\n ? { legacyPasswordMigrationOnStartup: environment.legacyPasswordMigrationOnStartup }\n : {}),\n ...(environment.autoUpdateDatabase !== undefined ? { autoUpdateDatabase: environment.autoUpdateDatabase } : {}),\n jwtOptions: environment.jwt,\n emailOptions: environment.email,\n ...(environment.http ? { http: environment.http } : {}),\n ...(environment.authCookie ? { authCookie: environment.authCookie } : {}),\n ...(environment.publicAuthRateLimit ? { publicAuthRateLimit: environment.publicAuthRateLimit } : {}),\n ...(environment.publicRegistration ? { publicRegistration: environment.publicRegistration } : {}),\n ...(environment.federatedAuthentication ? { federatedAuthentication: environment.federatedAuthentication } : {}),\n ...(environment.extensionFieldsOptions ? { extensionFieldsOptions: environment.extensionFieldsOptions } : {}),\n ...(environment.serviceTokens ? { serviceTokens: environment.serviceTokens } : {}),\n ...(environment.catalogScripts ? { catalogScripts: environment.catalogScripts } : {}),\n ...(environment.systemSeeds ? { systemSeeds: environment.systemSeeds } : {}),\n ...(environment.adminBootstrap ? { adminBootstrap: environment.adminBootstrap } : {}),\n ...(environment.openapi ? { openapi: environment.openapi } : {}),\n ...(environment.consoleEntry ? { consoleEntry: environment.consoleEntry } : {}),\n ...(environment.edgeRateLimit ? { edgeRateLimit: environment.edgeRateLimit } : {}),\n ...(environment.sessionRevocation ? { sessionRevocation: environment.sessionRevocation } : {}),\n ...(environment.accessTokenRevocation ? { accessTokenRevocation: environment.accessTokenRevocation } : {}),\n ...(environment.refreshTokenRotation ? { refreshTokenRotation: environment.refreshTokenRotation } : {}),\n ...(environment.metrics ? { metrics: environment.metrics } : {}),\n ...(environment.audit ? { audit: environment.audit } : {}),\n ...(environment.bufferLogs !== undefined ? { bufferLogs: environment.bufferLogs } : {}),\n tenantId: environment.tenant.id,\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction runtimeIssues(environment: unknown, file: string): string[] {\n const issues: string[] = [];\n if (!isRecord(environment)) {\n return [`${file}: il file deve esportare un oggetto (default export o export environment).`];\n }\n const tenant = environment.tenant;\n if (!isRecord(tenant)) {\n issues.push(`${file}: tenant mancante.`);\n } else {\n if (typeof tenant.id !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(tenant.id)) {\n issues.push(`${file}: tenant.id non valido (minuscole, numeri, - e _, max 63).`);\n }\n if (tenant.hosts !== undefined && (!Array.isArray(tenant.hosts) || tenant.hosts.some((host) => typeof host !== 'string'))) {\n issues.push(`${file}: tenant.hosts deve essere un array di stringhe.`);\n }\n if (tenant.path !== undefined && typeof tenant.path !== 'string') {\n issues.push(`${file}: tenant.path deve essere una stringa.`);\n }\n if (\n tenant.port !== undefined\n && (typeof tenant.port !== 'number' || !Number.isInteger(tenant.port) || tenant.port < 1 || tenant.port > 65535)\n ) {\n issues.push(`${file}: tenant.port non valida.`);\n }\n }\n\n const database = environment.database;\n if (!isRecord(database)) {\n issues.push(`${file}: database mancante.`);\n } else {\n if (typeof database.host !== 'string' || database.host.trim() === '') {\n issues.push(`${file}: database.host obbligatorio.`);\n }\n if (typeof database.database !== 'string' || database.database.trim() === '') {\n issues.push(`${file}: database.database obbligatorio.`);\n }\n if (\n database.port !== undefined\n && (typeof database.port !== 'number' || !Number.isInteger(database.port) || database.port < 1 || database.port > 65535)\n ) {\n issues.push(`${file}: database.port non valida.`);\n }\n }\n\n if (!isRecord(environment.jwt)) issues.push(`${file}: jwt mancante.`);\n else {\n if (typeof environment.jwt.secret !== 'string' || environment.jwt.secret === '') {\n issues.push(`${file}: jwt.secret obbligatorio.`);\n }\n if (typeof environment.jwt.expiresIn !== 'string' || environment.jwt.expiresIn === '') {\n issues.push(`${file}: jwt.expiresIn obbligatorio.`);\n }\n }\n\n if (!isRecord(environment.email)) issues.push(`${file}: email mancante.`);\n if (typeof environment.encryptionKey !== 'string' || environment.encryptionKey === '') {\n issues.push(`${file}: encryptionKey obbligatoria.`);\n }\n if (typeof environment.confirmationEmailUrl !== 'string') {\n issues.push(`${file}: confirmationEmailUrl obbligatoria.`);\n }\n if (typeof environment.confirmationEmailReturnUrl !== 'string') {\n issues.push(`${file}: confirmationEmailReturnUrl obbligatoria.`);\n }\n return issues;\n}\n\nfunction loadEnvironmentModule(file: string): unknown {\n if (/\\.(ts|mts|cts)$/i.test(file)) {\n // Caricamento lazy: jiti serve solo ai file tenant TypeScript.\n const { createJiti } = require('jiti') as {\n createJiti: (\n id: string,\n options?: { interopDefault?: boolean; moduleCache?: boolean; fsCache?: boolean },\n ) => (id: string) => unknown;\n };\n const jiti = createJiti(file, { interopDefault: true, moduleCache: false, fsCache: false });\n return jiti(file);\n }\n return require(file);\n}\n\n/**\n * Carica un singolo file `*.environment.ts`.\n *\n * Usa jiti per eseguire TypeScript anche nel pacchetto compilato, senza\n * richiedere un passo di build separato per i file tenant.\n */\nexport function loadAccessiTenantEnvironmentFile(file: string): { tenant: AccessiTenant; environment: AccessiTenantEnvironment } {\n const absolute = resolve(file);\n let loaded: unknown;\n try {\n loaded = loadEnvironmentModule(absolute);\n } catch (error) {\n throw new AccessiTenantConfigurationError([\n `${basename(file)}: impossibile caricare il file (${error instanceof Error ? error.message : String(error)}).`,\n ]);\n }\n\n const record = isRecord(loaded) ? loaded : {};\n const environment = (record.default ?? record.environment ?? loaded) as unknown;\n const issues = runtimeIssues(environment, basename(file));\n if (issues.length > 0) throw new AccessiTenantConfigurationError(issues);\n\n const typed = environment as AccessiTenantEnvironment;\n const settings = typed.tenant;\n const options = accessiOptionsFromTenantEnvironment(typed);\n const validation = validateAccessiOptions(options);\n const validationIssues = validation.errors.map((issue) => `${basename(file)}: ${issue.code}: ${issue.message}`);\n if (validationIssues.length > 0) throw new AccessiTenantConfigurationError(validationIssues);\n\n const hosts = (settings.hosts ?? []).map(normalizeHost).filter(Boolean);\n const path = normalizeTenantPath(settings.path);\n if (hosts.length === 0 && !path) {\n throw new AccessiTenantConfigurationError([\n `${basename(file)}: configurare tenant.hosts oppure tenant.path.`,\n ]);\n }\n\n const tenant: AccessiTenant = {\n id: settings.id,\n hosts,\n ...(path ? { path } : {}),\n enabled: settings.enabled !== false,\n ...(settings.description ? { description: settings.description } : {}),\n ...(settings.port ? { port: settings.port } : {}),\n file: basename(file),\n environmentFile: absolute,\n options,\n };\n return { tenant, environment: typed };\n}\n\n/**\n * Carica i tenant da una cartella di file `<id>.environment.ts`.\n *\n * La validazione e aggregata: un errore blocca l'intera cartella, cosi non si\n * avvia un tenant con configurazione parziale. Nessun segreto viene stampato.\n */\nexport function loadAccessiTenants(options: LoadAccessiTenantsOptions): AccessiTenantRegistry {\n if (!options?.directory) throw new Error('loadAccessiTenants: directory obbligatoria.');\n const directory = resolve(options.directory);\n const issues: string[] = [];\n const warnings: string[] = [];\n\n let files: string[];\n try {\n files = readdirSync(directory, { withFileTypes: true })\n .filter((entry) => entry.isFile() && /\\.environment\\.(ts|mts|cts|js|mjs|cjs)$/.test(entry.name))\n .map((entry) => entry.name)\n .sort();\n } catch (error) {\n throw new AccessiTenantConfigurationError([\n `Cartella tenant non leggibile (${directory}): ${error instanceof Error ? error.message : String(error)}`,\n ]);\n }\n if (files.length === 0) {\n throw new AccessiTenantConfigurationError([`Nessun file *.environment.ts nella cartella ${directory}.`]);\n }\n\n const tenants: AccessiTenant[] = [];\n for (const file of files) {\n try {\n tenants.push(loadAccessiTenantEnvironmentFile(join(directory, file)).tenant);\n } catch (error) {\n if (error instanceof AccessiTenantConfigurationError) issues.push(...error.issues);\n else issues.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n const byId = new Map<string, AccessiTenant>();\n const hostsSeen = new Map<string, string>();\n const pathsSeen = new Map<string, string>();\n const jwtSecrets = new Map<string, string>();\n const issuerAudience = new Map<string, string>();\n const portsSeen = new Map<number, string>();\n for (const tenant of tenants) {\n if (byId.has(tenant.id)) {\n issues.push(`Tenant duplicato \"${tenant.id}\" (${tenant.file} e ${byId.get(tenant.id)?.file}).`);\n } else {\n byId.set(tenant.id, tenant);\n }\n for (const host of tenant.hosts) {\n const existing = hostsSeen.get(host);\n if (existing) issues.push(`Host \"${host}\" assegnato a \"${existing}\" e \"${tenant.id}\".`);\n else hostsSeen.set(host, tenant.id);\n }\n if (tenant.path) {\n const existing = pathsSeen.get(tenant.path);\n if (existing) issues.push(`Path \"${tenant.path}\" assegnato a \"${existing}\" e \"${tenant.id}\".`);\n else pathsSeen.set(tenant.path, tenant.id);\n }\n if (tenant.port !== undefined) {\n const existing = portsSeen.get(tenant.port);\n if (existing) issues.push(`Porta ${tenant.port} assegnata a \"${existing}\" e \"${tenant.id}\".`);\n else portsSeen.set(tenant.port, tenant.id);\n }\n\n // Un segreto JWT condiviso rende un token valido su piu tenant.\n const signingKey = tenant.options.jwtOptions.privateKey ?? tenant.options.jwtOptions.secret;\n const fingerprintKey = `${tenant.options.jwtOptions.algorithm ?? 'HS256'}|${signingKey}`;\n const existingKey = jwtSecrets.get(fingerprintKey);\n if (existingKey) {\n issues.push(`Chiave JWT condivisa tra \"${existingKey}\" e \"${tenant.id}\": usare segreti distinti.`);\n } else {\n jwtSecrets.set(fingerprintKey, tenant.id);\n }\n\n const issuer = tenant.options.jwtOptions.issuer ?? '(assente)';\n const audience = Array.isArray(tenant.options.jwtOptions.audience)\n ? tenant.options.jwtOptions.audience.join(',')\n : (tenant.options.jwtOptions.audience ?? '(assente)');\n const issuerKey = `${issuer}|${audience}`;\n const existingIssuer = issuerAudience.get(issuerKey);\n if (existingIssuer && options.allowSharedIssuer !== true) {\n warnings.push(`Issuer/audience \"${issuerKey}\" condivisi tra \"${existingIssuer}\" e \"${tenant.id}\".`);\n } else {\n issuerAudience.set(issuerKey, tenant.id);\n }\n }\n\n const enabled = tenants.filter((tenant) => tenant.enabled);\n if (enabled.length > 1) {\n const defaultTenant = enabled.find((tenant) => tenant.hosts.length === 0 && tenant.path === '/');\n if (defaultTenant) {\n issues.push(`Tenant \"${defaultTenant.id}\" intercetta tutte le richieste (path \"/\") insieme ad altri tenant.`);\n }\n }\n\n if (issues.length > 0) throw new AccessiTenantConfigurationError(issues);\n\n const registry: AccessiTenantRegistry = {\n directory,\n tenants,\n warnings,\n allowHeader: options.allowHeader === true,\n byId,\n resolve(input, resolveOptions) {\n const allowHeader = resolveOptions?.allowHeader ?? registry.allowHeader;\n const host = input.host ? normalizeHost(input.host.split(':')[0] ?? input.host) : undefined;\n if (host) {\n const byHost = enabled.find((tenant) => matchesHost(tenant, host));\n if (byHost) return { tenant: byHost, matchedBy: 'host' };\n }\n\n const requestPath = input.path ?? '/';\n const byPath = enabled\n .filter((tenant) => matchesPath(tenant, requestPath))\n .sort((left, right) => (right.path?.length ?? 0) - (left.path?.length ?? 0))[0];\n if (byPath && byPath.path !== '/') return { tenant: byPath, matchedBy: 'path' };\n\n if (allowHeader && input.header) {\n const headerId = input.header.trim().toLowerCase();\n const tenant = byId.get(headerId);\n if (tenant?.enabled) return { tenant, matchedBy: 'header' };\n }\n\n if (byPath) return { tenant: byPath, matchedBy: 'path' };\n return undefined;\n },\n summary() {\n return tenants.map((tenant) => {\n const database = tenant.options.databaseOptions as {\n host?: string;\n port?: number;\n database?: string;\n user?: string;\n };\n return {\n id: tenant.id,\n hosts: [...tenant.hosts],\n ...(tenant.path ? { path: tenant.path } : {}),\n enabled: tenant.enabled,\n ...(tenant.description ? { description: tenant.description } : {}),\n ...(tenant.port ? { port: tenant.port } : {}),\n file: tenant.file,\n database: {\n host: database.host,\n port: database.port,\n database: database.database,\n user: database.user,\n },\n issuer: tenant.options.jwtOptions.issuer,\n ...(tenant.options.jwtOptions.audience\n ? {\n audience: typeof tenant.options.jwtOptions.audience === 'string'\n ? tenant.options.jwtOptions.audience\n : tenant.options.jwtOptions.audience.join(','),\n }\n : {}),\n jwtAlgorithm: tenant.options.jwtOptions.algorithm ?? 'HS256',\n refreshEnabled: Boolean(tenant.options.jwtOptions.refreshExpiresIn),\n };\n });\n },\n };\n\n return registry;\n}\n"]}
|
|
@@ -1854,7 +1854,7 @@ function buildAiDigest() {
|
|
|
1854
1854
|
}
|
|
1855
1855
|
|
|
1856
1856
|
// src/accessi-module/Console/changelog.md
|
|
1857
|
-
var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## Non rilasciato\n\n### Cambiamenti\n- Import: gli entry point (`accessi/server`, `accessi/resource-server`, `accessi/client*`, `accessi/contracts*`, `accessi/openapi`, `accessi/console`, `accessi/ui`) sono la via consigliata. Root barrel e deep import restano supportati fino alla prossima major; mappa di migrazione in `docs/import-migration.md`.\n- Sicurezza HTTP: CSP configurabile con nonce per la console, `'unsafe-inline'` documentato solo per Swagger UI, header frame/referrer/cache personalizzabili; guida in `docs/security-headers.md`.\n- Autenticazione: modalita opzionale a cookie `HttpOnly` con CSRF double-submit, refresh da cookie e supporto console; Bearer resta il default. Guida in `docs/auth-cookies.md`.\n- Observability: metriche Prometheus di dominio per login, 2FA, refresh, rate limit, introspection, JWKS, revoche, ticket console e store Redis, con alert suggeriti in `docs/metrics.md`.\n- Audit: sink memory/Redis/HTTP/callback, redazione ricorsiva, retention, query ed export NDJSON admin in `docs/audit.md`.\n- Multi-tenancy: un database per tenant senza modifiche allo schema, file `<id>.environment.ts` tipizzati (`AccessiTenantEnvironment`), validazione isolamento, risoluzione host/path/header fidato, supervisore processi, store Redis con prefisso automatico e template systemd/Docker/Nginx/Traefik in `deploy/`. Documentazione in `docs/multi-tenancy.md`.\n- Resource server: aggiunti `createAccessiAuthMiddleware`, `createAccessiAuthGuard`, `createAccessiTokenVerifier` e `createAccessiIntrospectionClient` per backend fratelli che non possiedono il database Accessi. Supporta HS256, RS/ES con chiave pubblica PEM e JWKS con cache.\n- Introspection: nuovo `POST /api/accessi/auth/introspect` protetto da service token con scope `accessi:introspect`. Restituisce `active`, profilo utente e grant correnti.\n- JWT: `jwtOptions` accetta `algorithm`, `issuer`, `audience`, `privateKey`, `publicKey`, `keyId`, `jwksPath` e `previousPublicKeys`. Default HS256 invariato; RS/ES sono consigliati per la topologia multi-servizio.\n- JWKS: con RS/ES Accessi pubblica `/accessi/.well-known/jwks.json` (path configurabile) con chiave corrente e chiavi precedenti per la rotazione. I resource server possono usare `jwks.url`.\n- Console ticket: introdotto `ConsoleEntryStore`. Default in memoria; `createRedisConsoleEntryStore` salva il JWT cifrato in Redis, usa TTL e consume atomico per il monouso multi-istanza. Configurazione in `consoleEntry.store` e `consoleEntry.ttlMs`.\n- Rate limit: introdotto `RateLimitStore`. `createRedisRateLimitStore` usa `INCR` + `PEXPIRE` atomici per contatori condivisi in cluster. Configurazione in `publicAuthRateLimit.store` e `serviceTokens.rateLimitStore`; default in memoria invariato.\n- Edge rate limit: aggiunto `createAccessiEdgeRateLimitMiddleware` e `AccessiOptions.edgeRateLimit` per una soglia grossolana per IP davanti alle rotte Accessi. Fail closed se lo store non risponde; supporta lo stesso store Redis.\n- Servizio standalone: aggiunti `createAccessiService`, `startAccessiService`, `/health/live`, `/health/ready`, `/health/metrics` e shutdown graceful. Aggiunti gli script `accessi:serve` e `accessi:serve:dist` per l'esecuzione come processo Node.\n- Configurazione: aggiunti `validateAccessiOptions` e `assertValidAccessiOptions`; il servizio standalone li applica al bootstrap.\n- HTTP: aggiunte opzioni `http.cors`, `http.trustProxy`, `http.securityHeaders`. La JWKS viene montata automaticamente anche con `AccessiModule.forRoot` tramite `HttpAdapterHost`.\n- SSO: aggiunto `POST /api/accessi/federated-auth/exchange` protetto da service token con scope `accessi:federated:exchange` per scambiare `{ provider, subject }` verificati da un backend fratello.\n- Audit: aggiunto `auditAccessi` per eventi di sicurezza strutturati; resource server ed edge rate limit registrano fallimenti di autenticazione, blocchi e rate limit non disponibile.\n- Contratto: aggiunto `npm run check:accessi-openapi`, eseguito nel build, che blocca path fuori prefisso, operationId duplicati/mancanti, $ref rotti, path legacy e rimozioni di operation/schema rispetto al fingerprint versionato.\n- Contratto: aggiunto `npm run accessi:update-openapi-fingerprint` per aggiornare intenzionalmente il fingerprint dopo una modifica breaking approvata.\n- Console: aggiunto `consoleApiBaseUrl` per servire UI e API su domini diversi; il valore viene iniettato in `window.__ACCESSI_API_BASE_URL__`.\n- Chiavi: aggiunto `npm run accessi:generate-jwt-keys` per generare coppia RS256/ES256, `keyId` e JWKS pubblica.\n- Sessioni: aggiunta la revoca per utente con `SessionRevocationStore` memory/Redis e `POST /api/accessi/auth/revoke-sessions`. Resource server e guard controllano il cutoff `iat`.\n- Refresh token: con `jwtOptions.refreshExpiresIn` login, 2FA e SSO emettono anche `refreshToken` (typ=refresh) e `POST /api/accessi/auth/refresh` restituisce una nuova coppia access+refresh. Gli access token hanno ora `jti`.\n- Refresh rotation: con `refreshTokenRotation.store` il refresh token e monouso e la rotazione e atomica tra istanze; due refresh concorrenti producono un solo successo.\n- Contratto: il fingerprint OpenAPI ora blocca anche la rimozione di operation/schema rispetto alla baseline versionata.\n- OpenAPI: aggiunto `mergeOpenApiDocuments` per unire il documento Accessi nel documento unico dell'host con conflitti espliciti.\n- Console: aggiunto `buildAccessiConsoleUrl` per linkare la console centrale senza mettere JWT nell'URL.\n- Rinomina: la configurazione e gli helper della documentazione usano `openapi`/`OpenApi` (`AccessiOptions.openapi`, `OpenApiSetupOptions`, `setupOpenApi`, `DEFAULT_ACCESSI_OPENAPI`); i vecchi nomi `swagger` non sono piu esportati.\n- Token Accessi: `TokenResult.expiresIn` riporta sempre la validita effettiva in secondi (es. `86400`), anche quando `jwtOptions.expiresIn` usa un formato leggibile come `24h`. I client possono quindi esporre `expires_in` anche per il token di login normale.\n- Console: nuova funzione **Login as** per superutenti e admin. Dalla lista utenti si genera un URL verso l'applicazione scelta con `?loginAsToken=<jwt>`; il token dura quanto la sessione Accessi standard (`jwtOptions.expiresIn`), registra l'attore nelle claim `act`/`loginAs` e resta soggetto alla gerarchia ADMIN > SUPER. Endpoint: `POST /api/accessi/user/login-as/:codiceUtente`.\n- Console: rimosse le sezioni **Filtri utente** e **Tipi filtro**. I filtri applicativi sono deprecati e non vengono pi\xF9 letti n\xE9 scritti dalla console; le tabelle restano invariate e la gestione torna manuale sul database.\n- Console: le conferme operative (es. \"Ruoli aggiornati\") ora compaiono come notifiche in alto a destra e scompaiono automaticamente.\n- Console: il salvataggio del profilo non invia pi\xF9 i campi della tabella `FILTRI`, evitando payload rifiutati dal backend. `UserService` tocca `FILTRI` solo se il chiamante invia esplicitamente almeno un campo legacy.\n- Console: scheda utente riorganizzata. \"Indietro\" resta nella barra superiore; stato registrazione, disattivazione ed eliminazione definitiva sono in un form coerente \"Stato e sicurezza\", con icone e tooltip allineati.\n- Console: pagina Changelog a larghezza piena, con intestazioni di versione evidenziate e sezioni orizzontali leggibili.\n- Seed `npm run accessi:seed-admin`: crea o aggiorna un admin completo (`FLGSUPER = 1` e `FLGADMIN = 1`) e corregge il nome colonna usato in precedenza (`FLGADMIN`).\n- Console: la pagina di default nella scheda utente \xE8 ora un autocomplete con ricerca. Propone tutte le rotte del catalogo menu, si filtra scrivendo ed \xE8 ancora possibile inserire un valore libero.\n- Autorizzazioni: i grant effettivi sommano ruoli e permessi diretti prendendo il livello massimo per menu. Un permesso diretto con livello inferiore (o 0) non annulla pi\xF9 il grant del ruolo.\n- Ruoli legacy: un menu senza descrizione non viene pi\xF9 scartato e un `TIPABI` mancante viene letto come livello 10, evitando utenti con ruolo ma senza accesso.\n- Grant dei ruoli arricchiti con i metadati di navigazione del catalogo menu (`pagina`, icona, gruppo, tipo): il frontend riceve gli stessi campi dei grant diretti e pu\xF2 costruire correttamente menu e rotte.\n- Sicurezza: `GET /api/accessi/permission/grants/:codiceUtente` accetta solo un JWT Accessi valido e consente la lettura solo al proprietario o a chi pu\xF2 gestire quel target (gerarchia ADMIN > SUPER). Un superutente non pu\xF2 leggere i grant di un admin.\n\n## 2.0.0-dev.50 (2026-10-15)\n\n### Cambiamenti\n- ORM: `Row` e `DbValue` sono i tipi predefiniti di `Orm.query` e `Orm.execute`; `transactionQuery` usa `Row[]`. Gli usi generici non devono pi\xF9 dichiarare `Record<string, unknown>`.\n- Build: il contratto OpenAPI generato (`src/accessi-module/openapi/`) \xE8 ignorato da Git e viene rigenerato da `npm run generate:accessi-openapi`.\n\n## 2.0.0-dev.49 (2026-10-15)\n\n### Cambiamenti\n- Documentazione HTTP rinominata da Swagger a **OpenAPI**: il modulo unificato espone `/accessi/openapi` e `/accessi/openapi.json`; `setupSwagger` usa `/openapi` come default.\n- Il path \xE8 configurabile tramite `EmilsoftwareOptions.swagger` (utile per far convivere pi\xF9 documentazioni).\n- Allineati log di avvio, Wiki e dispatch delle rotte del modulo.\n- L'alias storico `<path>-json` resta supportato.\n\n### Note\n- I consumer che aprivano `/swagger` o `/accessi/swagger` devono aggiornare l'URL alla nuova documentazione OpenAPI.\n\n## 2.0.0-dev.48 (2026-10-13)\n\n### Correzioni\n- `RestUtilities.sendErrorMessage` restituisce status HTTP specifici (400/401/403/4xx/5xx) e codici generici (`VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `INTERNAL_ERROR`, `REQUEST_ERROR`), senza usare codici `ACCESSI_*` nel business helper.\n- Il messaggio restituito \xE8 quello reale dell'eccezione; gli errori testuali di validazione vengono riconosciuti come 400.\n\n## 2.0.0-dev.47 (2026-10-13)\n\n### Correzioni\n- Errore schema database reso generico: codice `DATABASE_SCHEMA_OUTDATED` e messaggio senza riferimenti specifici ad Accessi.\n\n## 2.0.0-dev.46 (2026-10-12)\n\n### Correzioni\n- `sendBaseResponse` resta retro-compatibile quando il terzo argomento \xE8 un tag stringa invece dello status HTTP.\n- Gestita la serializzazione dei `bigint`; gli errori di risposta vengono registrati con stack tramite `sendErrorMessage`.\n- `AccessiHttpExceptionFilter` registra gli errori con stack per facilitare la diagnosi.\n\n## 2.0.0-dev.45 (2026-10-12)\n\n### Correzioni\n- `set-gdpr` e l'aggiornamento di `flagGdpr` sono idempotenti: `UPDATE OR INSERT` su `UTENTI_GDPR` con `MATCHING (CODUTE, DATACC)`.\n- Evitate violazioni di chiave primaria e risposte 500 quando il consenso GDPR viene registrato pi\xF9 volte nello stesso giorno.\n\n## 2.0.0-dev.44\n\n### Aggiunte\n- Endpoint pubblico `GET /api/accessi/auth/bootstrap-admin/status` (`getAdminBootstrapStatus`): la console mostra la sezione \"Crea utente admin\" **solo** se `adminBootstrap.enabled` \xE8 `true`.\n\n### Correzioni\n- Rimossi i caratteri corrotti (mojibake UTF-8 \u2194 Windows-1252) da DTO, servizi, controller, console, OpenAPI e client generato (es. \"L'email \xE8 gi\xE0 associata a un utente Accessi.\").\n\n## 2.0.0-dev.43\n\n### Cambiamenti\n- Gerarchia dei ruoli formalizzata **ADMIN > SUPER > utente**: un superutente non puo' gestire un admin su `update-user`, `delete-user` (soft e hard), `set-password`, `force-password-reset`, `set-stato`, `assign-roles` e `assign-permissions` (`ensureCanManageTargetUser`, 403 se violato).\n- Guard **ultimo admin attivo**: non puo' essere disattivato, bloccato, eliminato o privato del flag Admin (400), per evitare il lockout della console.\n- Creazione utente (`create-managed-user` e registrazione pubblica) resa **atomica** rispetto all'invio email: fail-fast se SMTP non configurato e rollback dell'utente creato se l'invio fallisce (`ACCESSI_USER_INVITE_EMAIL_FAILED`).\n- Console: form password separato (Invio imposta la password invece di salvare il profilo), scheda utente in sola lettura per target di rango superiore.\n- SSO: anche il superutente puo' creare utenti SSO e gestirne identita/policy (`federated-auth/users/*`) sugli utenti non-admin; il catalogo dei provider SSO resta riservato agli admin.\n- Coerenza: i filtri utente (`saveFiltriUtente`) rispettano la gerarchia; il flag GDPR impostato dal profilo allinea anche `DATGDPR`; la scheda in sola lettura disabilita anche il collegamento SSO.\n- Eliminazione definitiva: se l'utente ha dati collegati (tabelle esterne/FK) risponde 400 con `code: ACCESSI_USER_DELETE_HAS_REFERENCES` e messaggio esplicito, suggerendo il soft delete (la hard delete resta invariata quando non ci sono vincoli).\n- Consenso GDPR completo: `update-user` con `flagGdpr` scrive anche la riga storica `UTENTI_GDPR`; in console il pulsante \"Registra consenso GDPR\" usa `set-gdpr`.\n- Console: aggiunto il pulsante \"Invia reset password legacy\" (super e admin).\n- Console: **auto-ingresso dal frontend** tramite ticket monouso (`POST /api/accessi/console/entry` + apertura di `#entry=<ticket>`), senza JWT nell'URL; il ticket e' legato all'utente, dura 60s, e' monouso e lo scambio e' protetto da rate limit (`publicAuthRateLimit.consoleEntry`).\n\n## 2.0.0-dev.42\n\n### Aggiunte\n- Gestione dei flag di privilegio dalla console Accessi:\n - un admin pu\xF2 assegnare o togliere sia `FLGADMIN` sia `FLGSUPER`;\n - un superutente senza `FLGADMIN` pu\xF2 gestire solo `FLGSUPER`;\n - i flag sono impostabili in creazione (utente locale o SSO) e in modifica profilo.\n- Eliminazione utenti con due modalit\xE0:\n - soft delete (`delete-user`, `STAREG=DELETE`) per admin e superutenti;\n - eliminazione definitiva (`delete-user-permanent`, solo admin) che rimuove in transazione utente e record collegati.\n- Gestione completa dell'utente dalla console (admin): anagrafica estesa (cellulare, lingua, avatar, metadata JSON), GDPR, scadenza password, tutti i filtri (`FILTRI`) e **impostazione diretta della password** (`POST /api/accessi/user/set-password/:codiceUtente`), senza accesso al database.\n- Scritture utente idempotenti: `UTENTI_CONFIG` in `UPDATE OR INSERT ... MATCHING (CODUTE)`; i campi non inviati restano invariati.\n- Policy password moderna quando si imposta una password (reset, set-password admin, bootstrap): minimo 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comune. In caso di violazione risposta 400 con `code: ACCESSI_WEAK_PASSWORD` e `details`. La policy **non** si applica al login, cos\xEC le password preesistenti deboli continuano a funzionare.\n\n### Correzioni\n- Il backend verifica i flag in base al ruolo dell'utente connesso (`ensurePrivilegeFlagChanges`): inviare un flag non consentito restituisce 403.\n\n## 2.0.0-dev.33\n\n### Cambiamenti\n- Rimosse le logiche applicative residue dallo schema Accessi:\n - `MENU_GRP.NOMECAMPO`\n - `UTENTI.ENABLEIA`\n - `UTENTI_CONFIG.PAGDEF`\n - `UTENTI.CELLUTE`\n- Telefono canonico: `UTENTI_CONFIG.CELLULARE` (allargato a `VARCHAR(30)`).\n- Migrazione automatica: copia `UTENTI.CELLUTE` \u2192 `UTENTI_CONFIG.CELLULARE`, poi `DROP` delle colonne obsolete.\n- Ruoli chiariti:\n - `FLGSUPER`: superutente, riceve **tutte** le abilitazioni al livello massimo (30) dopo il login; in console vede **solo** la sezione **Utenti**.\n - `FLGADMIN`: accede a **tutta** la console (catalogo, token, SSO, utenti) ma **non** influisce sulle abilitazioni.\n- Aggiunta la sezione **Changelog** in console (sotto la Wiki).\n\n### Note\n- `FILTRI` e `FILTRI_TIPO` restano invariati (piano di migrazione futuro).\n- I backend che usavano i campi rimossi devono gestirli in tabelle proprie.\n\n## 2.0.0-dev.32\n\n### Cambiamenti\n- Rimosso il supporto ai flag applicativi in `UTENTI_CONFIG`: `FLGMOP`, `FLGPIANA`, `FLGADDETTI`, `FLGOSPITI`, `FLGPIANARFID`, `FLGCONTA`, `FLGCUBI`, `FLGCICLPASS`, `FLGDIPENDENTI`, `FLGINVENTARI`, `CAUMOV`, `NUMMAC`, `RAGSOCCLI`.\n- Rinominata la colonna `FLGADMINCONFIG` in `FLGADMIN` (con copia dei valori e `DROP`).\n\n## 2.0.0-dev.31\n\n### Aggiunte\n- Esposizione dei servizi Accessi all'host Express (`getAccessiModuleServices`) per delegare login, SSO e token senza reimplementarli.\n\n### Correzioni\n- `UserService.getUsers`: filtri estesi (`codiceUtente[]`, `numRep[]`, `codDipendente`, `cellulare`, `tipFil`, `flagSuper`).\n\n## 2.0.0-dev.24\n\n### Cambiamenti (breaking)\n- Chiavi JWT derivate per scopo (`access`, `reset`, `2fa`) e claim `typ` obbligatorio: gli access token precedenti non sono pi\xF9 validi.\n- Endpoint Allegati protetti di default.\n- Pool di connessioni attivo di default (`poolSize` 5).\n\n### Aggiunte\n- Service token macchina-a-macchina (gestione, guard, console).\n- CRUD completo dei cataloghi dalla console Accessi.\n- Supporto Firebird 2.5 nel migrator e nei test di integrazione.\n";
|
|
1857
|
+
var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## Non rilasciato\n\n### Cambiamenti\n- Import: gli entry point (`accessi/server`, `accessi/resource-server`, `accessi/client*`, `accessi/contracts*`, `accessi/openapi`, `accessi/console`, `accessi/ui`) sono la via consigliata. Root barrel e deep import restano supportati fino alla prossima major; mappa di migrazione in `docs/import-migration.md`.\n- Sicurezza HTTP: CSP configurabile con nonce per la console, `'unsafe-inline'` documentato solo per Swagger UI, header frame/referrer/cache personalizzabili; guida in `docs/security-headers.md`.\n- Autenticazione: modalita opzionale a cookie `HttpOnly` con CSRF double-submit, refresh da cookie e supporto console; Bearer resta il default. Guida in `docs/auth-cookies.md`.\n- Observability: metriche Prometheus di dominio per login, 2FA, refresh, rate limit, introspection, JWKS, revoche, ticket console e store Redis, con alert suggeriti in `docs/metrics.md`.\n- Audit: sink memory/Redis/HTTP/callback, redazione ricorsiva, retention, query ed export NDJSON admin in `docs/audit.md`.\n- Multi-tenancy: un database per tenant senza modifiche allo schema, file `<id>.environment.ts` tipizzati (`AccessiTenantEnvironment`), validazione isolamento, risoluzione host/path/header fidato, supervisore processi con `--watch` per aggiunta/rimozione/restart dei tenant, store Redis con prefisso automatico e template systemd/Docker/Nginx/Traefik in `deploy/`. Documentazione in `docs/multi-tenancy.md`.\n- Resource server: aggiunti `createAccessiAuthMiddleware`, `createAccessiAuthGuard`, `createAccessiTokenVerifier` e `createAccessiIntrospectionClient` per backend fratelli che non possiedono il database Accessi. Supporta HS256, RS/ES con chiave pubblica PEM e JWKS con cache.\n- Introspection: nuovo `POST /api/accessi/auth/introspect` protetto da service token con scope `accessi:introspect`. Restituisce `active`, profilo utente e grant correnti.\n- JWT: `jwtOptions` accetta `algorithm`, `issuer`, `audience`, `privateKey`, `publicKey`, `keyId`, `jwksPath` e `previousPublicKeys`. Default HS256 invariato; RS/ES sono consigliati per la topologia multi-servizio.\n- JWKS: con RS/ES Accessi pubblica `/accessi/.well-known/jwks.json` (path configurabile) con chiave corrente e chiavi precedenti per la rotazione. I resource server possono usare `jwks.url`.\n- Console ticket: introdotto `ConsoleEntryStore`. Default in memoria; `createRedisConsoleEntryStore` salva il JWT cifrato in Redis, usa TTL e consume atomico per il monouso multi-istanza. Configurazione in `consoleEntry.store` e `consoleEntry.ttlMs`.\n- Rate limit: introdotto `RateLimitStore`. `createRedisRateLimitStore` usa `INCR` + `PEXPIRE` atomici per contatori condivisi in cluster. Configurazione in `publicAuthRateLimit.store` e `serviceTokens.rateLimitStore`; default in memoria invariato.\n- Edge rate limit: aggiunto `createAccessiEdgeRateLimitMiddleware` e `AccessiOptions.edgeRateLimit` per una soglia grossolana per IP davanti alle rotte Accessi. Fail closed se lo store non risponde; supporta lo stesso store Redis.\n- Servizio standalone: aggiunti `createAccessiService`, `startAccessiService`, `/health/live`, `/health/ready`, `/health/metrics` e shutdown graceful. Aggiunti gli script `accessi:serve` e `accessi:serve:dist` per l'esecuzione come processo Node.\n- Configurazione: aggiunti `validateAccessiOptions` e `assertValidAccessiOptions`; il servizio standalone li applica al bootstrap.\n- HTTP: aggiunte opzioni `http.cors`, `http.trustProxy`, `http.securityHeaders`. La JWKS viene montata automaticamente anche con `AccessiModule.forRoot` tramite `HttpAdapterHost`.\n- SSO: aggiunto `POST /api/accessi/federated-auth/exchange` protetto da service token con scope `accessi:federated:exchange` per scambiare `{ provider, subject }` verificati da un backend fratello.\n- Audit: aggiunto `auditAccessi` per eventi di sicurezza strutturati; resource server ed edge rate limit registrano fallimenti di autenticazione, blocchi e rate limit non disponibile.\n- Contratto: aggiunto `npm run check:accessi-openapi`, eseguito nel build, che blocca path fuori prefisso, operationId duplicati/mancanti, $ref rotti, path legacy e rimozioni di operation/schema rispetto al fingerprint versionato.\n- Contratto: aggiunto `npm run accessi:update-openapi-fingerprint` per aggiornare intenzionalmente il fingerprint dopo una modifica breaking approvata.\n- Console: aggiunto `consoleApiBaseUrl` per servire UI e API su domini diversi; il valore viene iniettato in `window.__ACCESSI_API_BASE_URL__`.\n- Chiavi: aggiunto `npm run accessi:generate-jwt-keys` per generare coppia RS256/ES256, `keyId` e JWKS pubblica.\n- Sessioni: aggiunta la revoca per utente con `SessionRevocationStore` memory/Redis e `POST /api/accessi/auth/revoke-sessions`. Resource server e guard controllano il cutoff `iat`.\n- Refresh token: con `jwtOptions.refreshExpiresIn` login, 2FA e SSO emettono anche `refreshToken` (typ=refresh) e `POST /api/accessi/auth/refresh` restituisce una nuova coppia access+refresh. Gli access token hanno ora `jti`.\n- Refresh rotation: con `refreshTokenRotation.store` il refresh token e monouso e la rotazione e atomica tra istanze; due refresh concorrenti producono un solo successo.\n- Contratto: il fingerprint OpenAPI ora blocca anche la rimozione di operation/schema rispetto alla baseline versionata.\n- OpenAPI: aggiunto `mergeOpenApiDocuments` per unire il documento Accessi nel documento unico dell'host con conflitti espliciti.\n- Console: aggiunto `buildAccessiConsoleUrl` per linkare la console centrale senza mettere JWT nell'URL.\n- Rinomina: la configurazione e gli helper della documentazione usano `openapi`/`OpenApi` (`AccessiOptions.openapi`, `OpenApiSetupOptions`, `setupOpenApi`, `DEFAULT_ACCESSI_OPENAPI`); i vecchi nomi `swagger` non sono piu esportati.\n- Token Accessi: `TokenResult.expiresIn` riporta sempre la validita effettiva in secondi (es. `86400`), anche quando `jwtOptions.expiresIn` usa un formato leggibile come `24h`. I client possono quindi esporre `expires_in` anche per il token di login normale.\n- Console: nuova funzione **Login as** per superutenti e admin. Dalla lista utenti si genera un URL verso l'applicazione scelta con `?loginAsToken=<jwt>`; il token dura quanto la sessione Accessi standard (`jwtOptions.expiresIn`), registra l'attore nelle claim `act`/`loginAs` e resta soggetto alla gerarchia ADMIN > SUPER. Endpoint: `POST /api/accessi/user/login-as/:codiceUtente`.\n- Console: rimosse le sezioni **Filtri utente** e **Tipi filtro**. I filtri applicativi sono deprecati e non vengono pi\xF9 letti n\xE9 scritti dalla console; le tabelle restano invariate e la gestione torna manuale sul database.\n- Console: le conferme operative (es. \"Ruoli aggiornati\") ora compaiono come notifiche in alto a destra e scompaiono automaticamente.\n- Console: il salvataggio del profilo non invia pi\xF9 i campi della tabella `FILTRI`, evitando payload rifiutati dal backend. `UserService` tocca `FILTRI` solo se il chiamante invia esplicitamente almeno un campo legacy.\n- Console: scheda utente riorganizzata. \"Indietro\" resta nella barra superiore; stato registrazione, disattivazione ed eliminazione definitiva sono in un form coerente \"Stato e sicurezza\", con icone e tooltip allineati.\n- Console: pagina Changelog a larghezza piena, con intestazioni di versione evidenziate e sezioni orizzontali leggibili.\n- Seed `npm run accessi:seed-admin`: crea o aggiorna un admin completo (`FLGSUPER = 1` e `FLGADMIN = 1`) e corregge il nome colonna usato in precedenza (`FLGADMIN`).\n- Console: la pagina di default nella scheda utente \xE8 ora un autocomplete con ricerca. Propone tutte le rotte del catalogo menu, si filtra scrivendo ed \xE8 ancora possibile inserire un valore libero.\n- Autorizzazioni: i grant effettivi sommano ruoli e permessi diretti prendendo il livello massimo per menu. Un permesso diretto con livello inferiore (o 0) non annulla pi\xF9 il grant del ruolo.\n- Ruoli legacy: un menu senza descrizione non viene pi\xF9 scartato e un `TIPABI` mancante viene letto come livello 10, evitando utenti con ruolo ma senza accesso.\n- Grant dei ruoli arricchiti con i metadati di navigazione del catalogo menu (`pagina`, icona, gruppo, tipo): il frontend riceve gli stessi campi dei grant diretti e pu\xF2 costruire correttamente menu e rotte.\n- Sicurezza: `GET /api/accessi/permission/grants/:codiceUtente` accetta solo un JWT Accessi valido e consente la lettura solo al proprietario o a chi pu\xF2 gestire quel target (gerarchia ADMIN > SUPER). Un superutente non pu\xF2 leggere i grant di un admin.\n\n## 2.0.0-dev.50 (2026-10-15)\n\n### Cambiamenti\n- ORM: `Row` e `DbValue` sono i tipi predefiniti di `Orm.query` e `Orm.execute`; `transactionQuery` usa `Row[]`. Gli usi generici non devono pi\xF9 dichiarare `Record<string, unknown>`.\n- Build: il contratto OpenAPI generato (`src/accessi-module/openapi/`) \xE8 ignorato da Git e viene rigenerato da `npm run generate:accessi-openapi`.\n\n## 2.0.0-dev.49 (2026-10-15)\n\n### Cambiamenti\n- Documentazione HTTP rinominata da Swagger a **OpenAPI**: il modulo unificato espone `/accessi/openapi` e `/accessi/openapi.json`; `setupSwagger` usa `/openapi` come default.\n- Il path \xE8 configurabile tramite `EmilsoftwareOptions.swagger` (utile per far convivere pi\xF9 documentazioni).\n- Allineati log di avvio, Wiki e dispatch delle rotte del modulo.\n- L'alias storico `<path>-json` resta supportato.\n\n### Note\n- I consumer che aprivano `/swagger` o `/accessi/swagger` devono aggiornare l'URL alla nuova documentazione OpenAPI.\n\n## 2.0.0-dev.48 (2026-10-13)\n\n### Correzioni\n- `RestUtilities.sendErrorMessage` restituisce status HTTP specifici (400/401/403/4xx/5xx) e codici generici (`VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `INTERNAL_ERROR`, `REQUEST_ERROR`), senza usare codici `ACCESSI_*` nel business helper.\n- Il messaggio restituito \xE8 quello reale dell'eccezione; gli errori testuali di validazione vengono riconosciuti come 400.\n\n## 2.0.0-dev.47 (2026-10-13)\n\n### Correzioni\n- Errore schema database reso generico: codice `DATABASE_SCHEMA_OUTDATED` e messaggio senza riferimenti specifici ad Accessi.\n\n## 2.0.0-dev.46 (2026-10-12)\n\n### Correzioni\n- `sendBaseResponse` resta retro-compatibile quando il terzo argomento \xE8 un tag stringa invece dello status HTTP.\n- Gestita la serializzazione dei `bigint`; gli errori di risposta vengono registrati con stack tramite `sendErrorMessage`.\n- `AccessiHttpExceptionFilter` registra gli errori con stack per facilitare la diagnosi.\n\n## 2.0.0-dev.45 (2026-10-12)\n\n### Correzioni\n- `set-gdpr` e l'aggiornamento di `flagGdpr` sono idempotenti: `UPDATE OR INSERT` su `UTENTI_GDPR` con `MATCHING (CODUTE, DATACC)`.\n- Evitate violazioni di chiave primaria e risposte 500 quando il consenso GDPR viene registrato pi\xF9 volte nello stesso giorno.\n\n## 2.0.0-dev.44\n\n### Aggiunte\n- Endpoint pubblico `GET /api/accessi/auth/bootstrap-admin/status` (`getAdminBootstrapStatus`): la console mostra la sezione \"Crea utente admin\" **solo** se `adminBootstrap.enabled` \xE8 `true`.\n\n### Correzioni\n- Rimossi i caratteri corrotti (mojibake UTF-8 \u2194 Windows-1252) da DTO, servizi, controller, console, OpenAPI e client generato (es. \"L'email \xE8 gi\xE0 associata a un utente Accessi.\").\n\n## 2.0.0-dev.43\n\n### Cambiamenti\n- Gerarchia dei ruoli formalizzata **ADMIN > SUPER > utente**: un superutente non puo' gestire un admin su `update-user`, `delete-user` (soft e hard), `set-password`, `force-password-reset`, `set-stato`, `assign-roles` e `assign-permissions` (`ensureCanManageTargetUser`, 403 se violato).\n- Guard **ultimo admin attivo**: non puo' essere disattivato, bloccato, eliminato o privato del flag Admin (400), per evitare il lockout della console.\n- Creazione utente (`create-managed-user` e registrazione pubblica) resa **atomica** rispetto all'invio email: fail-fast se SMTP non configurato e rollback dell'utente creato se l'invio fallisce (`ACCESSI_USER_INVITE_EMAIL_FAILED`).\n- Console: form password separato (Invio imposta la password invece di salvare il profilo), scheda utente in sola lettura per target di rango superiore.\n- SSO: anche il superutente puo' creare utenti SSO e gestirne identita/policy (`federated-auth/users/*`) sugli utenti non-admin; il catalogo dei provider SSO resta riservato agli admin.\n- Coerenza: i filtri utente (`saveFiltriUtente`) rispettano la gerarchia; il flag GDPR impostato dal profilo allinea anche `DATGDPR`; la scheda in sola lettura disabilita anche il collegamento SSO.\n- Eliminazione definitiva: se l'utente ha dati collegati (tabelle esterne/FK) risponde 400 con `code: ACCESSI_USER_DELETE_HAS_REFERENCES` e messaggio esplicito, suggerendo il soft delete (la hard delete resta invariata quando non ci sono vincoli).\n- Consenso GDPR completo: `update-user` con `flagGdpr` scrive anche la riga storica `UTENTI_GDPR`; in console il pulsante \"Registra consenso GDPR\" usa `set-gdpr`.\n- Console: aggiunto il pulsante \"Invia reset password legacy\" (super e admin).\n- Console: **auto-ingresso dal frontend** tramite ticket monouso (`POST /api/accessi/console/entry` + apertura di `#entry=<ticket>`), senza JWT nell'URL; il ticket e' legato all'utente, dura 60s, e' monouso e lo scambio e' protetto da rate limit (`publicAuthRateLimit.consoleEntry`).\n\n## 2.0.0-dev.42\n\n### Aggiunte\n- Gestione dei flag di privilegio dalla console Accessi:\n - un admin pu\xF2 assegnare o togliere sia `FLGADMIN` sia `FLGSUPER`;\n - un superutente senza `FLGADMIN` pu\xF2 gestire solo `FLGSUPER`;\n - i flag sono impostabili in creazione (utente locale o SSO) e in modifica profilo.\n- Eliminazione utenti con due modalit\xE0:\n - soft delete (`delete-user`, `STAREG=DELETE`) per admin e superutenti;\n - eliminazione definitiva (`delete-user-permanent`, solo admin) che rimuove in transazione utente e record collegati.\n- Gestione completa dell'utente dalla console (admin): anagrafica estesa (cellulare, lingua, avatar, metadata JSON), GDPR, scadenza password, tutti i filtri (`FILTRI`) e **impostazione diretta della password** (`POST /api/accessi/user/set-password/:codiceUtente`), senza accesso al database.\n- Scritture utente idempotenti: `UTENTI_CONFIG` in `UPDATE OR INSERT ... MATCHING (CODUTE)`; i campi non inviati restano invariati.\n- Policy password moderna quando si imposta una password (reset, set-password admin, bootstrap): minimo 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comune. In caso di violazione risposta 400 con `code: ACCESSI_WEAK_PASSWORD` e `details`. La policy **non** si applica al login, cos\xEC le password preesistenti deboli continuano a funzionare.\n\n### Correzioni\n- Il backend verifica i flag in base al ruolo dell'utente connesso (`ensurePrivilegeFlagChanges`): inviare un flag non consentito restituisce 403.\n\n## 2.0.0-dev.33\n\n### Cambiamenti\n- Rimosse le logiche applicative residue dallo schema Accessi:\n - `MENU_GRP.NOMECAMPO`\n - `UTENTI.ENABLEIA`\n - `UTENTI_CONFIG.PAGDEF`\n - `UTENTI.CELLUTE`\n- Telefono canonico: `UTENTI_CONFIG.CELLULARE` (allargato a `VARCHAR(30)`).\n- Migrazione automatica: copia `UTENTI.CELLUTE` \u2192 `UTENTI_CONFIG.CELLULARE`, poi `DROP` delle colonne obsolete.\n- Ruoli chiariti:\n - `FLGSUPER`: superutente, riceve **tutte** le abilitazioni al livello massimo (30) dopo il login; in console vede **solo** la sezione **Utenti**.\n - `FLGADMIN`: accede a **tutta** la console (catalogo, token, SSO, utenti) ma **non** influisce sulle abilitazioni.\n- Aggiunta la sezione **Changelog** in console (sotto la Wiki).\n\n### Note\n- `FILTRI` e `FILTRI_TIPO` restano invariati (piano di migrazione futuro).\n- I backend che usavano i campi rimossi devono gestirli in tabelle proprie.\n\n## 2.0.0-dev.32\n\n### Cambiamenti\n- Rimosso il supporto ai flag applicativi in `UTENTI_CONFIG`: `FLGMOP`, `FLGPIANA`, `FLGADDETTI`, `FLGOSPITI`, `FLGPIANARFID`, `FLGCONTA`, `FLGCUBI`, `FLGCICLPASS`, `FLGDIPENDENTI`, `FLGINVENTARI`, `CAUMOV`, `NUMMAC`, `RAGSOCCLI`.\n- Rinominata la colonna `FLGADMINCONFIG` in `FLGADMIN` (con copia dei valori e `DROP`).\n\n## 2.0.0-dev.31\n\n### Aggiunte\n- Esposizione dei servizi Accessi all'host Express (`getAccessiModuleServices`) per delegare login, SSO e token senza reimplementarli.\n\n### Correzioni\n- `UserService.getUsers`: filtri estesi (`codiceUtente[]`, `numRep[]`, `codDipendente`, `cellulare`, `tipFil`, `flagSuper`).\n\n## 2.0.0-dev.24\n\n### Cambiamenti (breaking)\n- Chiavi JWT derivate per scopo (`access`, `reset`, `2fa`) e claim `typ` obbligatorio: gli access token precedenti non sono pi\xF9 validi.\n- Endpoint Allegati protetti di default.\n- Pool di connessioni attivo di default (`poolSize` 5).\n\n### Aggiunte\n- Service token macchina-a-macchina (gestione, guard, console).\n- CRUD completo dei cataloghi dalla console Accessi.\n- Supporto Firebird 2.5 nel migrator e nei test di integrazione.\n";
|
|
1858
1858
|
|
|
1859
1859
|
// src/accessi-module/Console/help.ts
|
|
1860
1860
|
var p2 = (text) => ({ kind: "p", text });
|
|
Binary file
|
|
@@ -144,7 +144,7 @@ tenant.
|
|
|
144
144
|
| Schema per tenant | Firebird non offre schema/search_path; sarebbe un database separato con altro nome. |
|
|
145
145
|
| Due `AccessiModule` nello stesso processo | La configurazione Nest è globale; audit/metriche/logger condividono stato. |
|
|
146
146
|
| Selezione tenant da header non fidato | Permetterebbe a un client di cambiare tenant e tentare leakage; consentito solo dietro proxy che sovrascrive l'header. |
|
|
147
|
-
| Hot reload dei file `.environment.ts` | Nest non smonta moduli a caldo; il
|
|
147
|
+
| Hot reload dei file `.environment.ts` | Nest non smonta moduli a caldo; il supervisore riavvia i processi dei tenant modificati, non l'intero servizio. |
|
|
148
148
|
|
|
149
149
|
## Conseguenze
|
|
150
150
|
|
|
@@ -152,7 +152,7 @@ Positive:
|
|
|
152
152
|
|
|
153
153
|
- isolamento forte: database, chiavi e processi separati;
|
|
154
154
|
- nessuna migrazione dello schema esistente;
|
|
155
|
-
- aggiunta tenant = nuovo file `.environment.ts`
|
|
155
|
+
- aggiunta tenant = nuovo file `.environment.ts` (il supervisore con `--watch` lo avvia senza riavvio manuale);
|
|
156
156
|
- rollback per tenant indipendente.
|
|
157
157
|
|
|
158
158
|
Negative:
|
|
@@ -166,8 +166,8 @@ Negative:
|
|
|
166
166
|
## Piano
|
|
167
167
|
|
|
168
168
|
1. **Fase 1 (questa)**: loader cartella, validazione isolamento, CLI
|
|
169
|
-
`accessi:tenants` (`--check`, `--list`, `--resolve`, `--serve`),
|
|
170
|
-
e test.
|
|
169
|
+
`accessi:tenants` (`--check`, `--list`, `--resolve`, `--serve`, `--watch`),
|
|
170
|
+
helper store Redis, documentazione e test.
|
|
171
171
|
2. **Fase 2**: template systemd/docker e reverse proxy host/path; health check
|
|
172
172
|
per tenant nel supervisore.
|
|
173
173
|
3. **Fase 3**: migrazione del deployment single-tenant esistente: creare
|
package/docs/multi-tenancy.md
CHANGED
|
@@ -126,8 +126,25 @@ Avvia un processo figlio per ogni tenant abilitato:
|
|
|
126
126
|
- non riavvia automaticamente un figlio: un loop su configurazione rotta è
|
|
127
127
|
peggio di un processo che resta giù; usare systemd/docker/Kubernetes.
|
|
128
128
|
|
|
129
|
-
Il supervisore legge la cartella all'avvio
|
|
130
|
-
|
|
129
|
+
Il supervisore legge la cartella all'avvio. Con `--watch` osserva i file
|
|
130
|
+
`*.environment.ts` e riapplica la configurazione:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm run accessi:tenants -- --dir ./tenants --serve --watch
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Regole:
|
|
137
|
+
|
|
138
|
+
- un file invalido annulla il reload e lascia attivi i tenant correnti;
|
|
139
|
+
- i tenant invariati non vengono riavviati;
|
|
140
|
+
- un tenant modificato viene riavviato sulla stessa porta (breve interruzione);
|
|
141
|
+
- aggiunte e rimozioni avviano/fermano solo il tenant interessato;
|
|
142
|
+
- il polling è di 1 secondo con debounce di 200 ms (`pollIntervalMs`/
|
|
143
|
+
`debounceMs` nell'API del supervisore).
|
|
144
|
+
|
|
145
|
+
Senza `--watch` un cambiamento richiede il riavvio del supervisore. Non c'è
|
|
146
|
+
montaggio/smontaggio di moduli Nest a caldo: il reload riavvia i processi
|
|
147
|
+
interessati, non l'intero servizio.
|
|
131
148
|
|
|
132
149
|
### systemd (consigliato in produzione)
|
|
133
150
|
|
|
@@ -332,7 +349,8 @@ schema.
|
|
|
332
349
|
## Limiti
|
|
333
350
|
|
|
334
351
|
- Un processo per tenant: più RAM, più pool Firebird, più connessioni SMTP.
|
|
335
|
-
-
|
|
352
|
+
- Il reload con `--watch` riavvia i processi modificati sulla stessa porta: c'è
|
|
353
|
+
una breve interruzione per il singolo tenant, non per gli altri.
|
|
336
354
|
- Migrazioni e backup per database.
|
|
337
355
|
- La risoluzione via header è ammessa solo dietro proxy fidato; il client non
|
|
338
356
|
deve poter scegliere il tenant.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emilsoftware-utilities",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.83",
|
|
4
4
|
"description": "Utilities for EmilSoftware",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -141,8 +141,8 @@
|
|
|
141
141
|
"lint": "node scripts/lint.cjs",
|
|
142
142
|
"check:deploy": "node scripts/check-deploy-templates.cjs",
|
|
143
143
|
"test": "npm run typecheck && npm run lint && npm run test:unit",
|
|
144
|
-
"test:unit": "node --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs test/token-revocation.cjs test/accessi-client.cjs test/accessi-client-scopes.cjs test/openapi-semantic-diff.cjs test/proxy-ip.cjs test/readiness.cjs test/jwt-key-rotation.cjs test/security-headers.cjs test/auth-cookies.cjs test/metrics.cjs test/audit.cjs test/import-entrypoints.cjs test/tenants.cjs test/tenant-stores.cjs test/deploy-templates.cjs",
|
|
145
|
-
"test:coverage": "node --experimental-test-coverage --test-coverage-lines=50 --test-coverage-functions=60 --test-coverage-branches=60 --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs test/token-revocation.cjs test/accessi-client.cjs test/accessi-client-scopes.cjs test/openapi-semantic-diff.cjs test/proxy-ip.cjs test/readiness.cjs test/jwt-key-rotation.cjs test/security-headers.cjs test/auth-cookies.cjs test/metrics.cjs test/audit.cjs test/import-entrypoints.cjs test/tenants.cjs test/tenant-stores.cjs test/deploy-templates.cjs",
|
|
144
|
+
"test:unit": "node --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs test/token-revocation.cjs test/accessi-client.cjs test/accessi-client-scopes.cjs test/openapi-semantic-diff.cjs test/proxy-ip.cjs test/readiness.cjs test/jwt-key-rotation.cjs test/security-headers.cjs test/auth-cookies.cjs test/metrics.cjs test/audit.cjs test/import-entrypoints.cjs test/tenants.cjs test/tenant-stores.cjs test/deploy-templates.cjs test/tenant-supervisor.cjs",
|
|
145
|
+
"test:coverage": "node --experimental-test-coverage --test-coverage-lines=50 --test-coverage-functions=60 --test-coverage-branches=60 --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs test/token-revocation.cjs test/accessi-client.cjs test/accessi-client-scopes.cjs test/openapi-semantic-diff.cjs test/proxy-ip.cjs test/readiness.cjs test/jwt-key-rotation.cjs test/security-headers.cjs test/auth-cookies.cjs test/metrics.cjs test/audit.cjs test/import-entrypoints.cjs test/tenants.cjs test/tenant-stores.cjs test/deploy-templates.cjs test/tenant-supervisor.cjs",
|
|
146
146
|
"test:accessi": "npm run test:unit",
|
|
147
147
|
"test:all": "npm test && npm run test:integration && npm run test:integration:redis",
|
|
148
148
|
"test:integration": "node test/integration/run-firebird.cjs 5",
|
|
Binary file
|