emilsoftware-utilities 1.8.0-dev.2 → 2.0.0-dev.1
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/README.MD +4 -0
- package/dist/Utilities.d.ts +18 -0
- package/dist/Utilities.js +90 -1
- package/dist/Utilities.js.map +1 -1
- package/dist/accessi-module/AccessiModule.d.ts +15 -0
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Controllers/AuthController.d.ts +4 -4
- package/dist/accessi-module/Controllers/AuthController.js +36 -21
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Controllers/EmailController.d.ts +3 -3
- package/dist/accessi-module/Controllers/EmailController.js +15 -15
- package/dist/accessi-module/Controllers/EmailController.js.map +1 -1
- package/dist/accessi-module/Controllers/FiltriController.d.ts +3 -3
- package/dist/accessi-module/Controllers/FiltriController.js +32 -15
- package/dist/accessi-module/Controllers/FiltriController.js.map +1 -1
- package/dist/accessi-module/Controllers/PermissionController.d.ts +12 -17
- package/dist/accessi-module/Controllers/PermissionController.js +140 -124
- package/dist/accessi-module/Controllers/PermissionController.js.map +1 -1
- package/dist/accessi-module/Controllers/UserController.d.ts +12 -11
- package/dist/accessi-module/Controllers/UserController.js +112 -79
- package/dist/accessi-module/Controllers/UserController.js.map +1 -1
- package/dist/accessi-module/Dtos/AssignPermissionsToUserRequest.js +6 -0
- package/dist/accessi-module/Dtos/AssignPermissionsToUserRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/AssignRolesToUserRequest.js +6 -0
- package/dist/accessi-module/Dtos/AssignRolesToUserRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/Permission.js +5 -0
- package/dist/accessi-module/Dtos/Permission.js.map +1 -1
- package/dist/accessi-module/Dtos/RegisterRequest.js +37 -1
- package/dist/accessi-module/Dtos/RegisterRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/Role.js +13 -2
- package/dist/accessi-module/Dtos/Role.js.map +1 -1
- package/dist/accessi-module/Dtos/SetStatoRegistrazione.js +6 -0
- package/dist/accessi-module/Dtos/SetStatoRegistrazione.js.map +1 -1
- package/dist/accessi-module/Dtos/UserDto.js +59 -1
- package/dist/accessi-module/Dtos/UserDto.js.map +1 -1
- package/dist/accessi-module/Services/AuthService/AuthService.d.ts +7 -3
- package/dist/accessi-module/Services/AuthService/AuthService.js +147 -25
- package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
- package/dist/accessi-module/Services/EmailService/EmailService.js +33 -10
- package/dist/accessi-module/Services/EmailService/EmailService.js.map +1 -1
- package/dist/accessi-module/Services/UserService/UserService.d.ts +15 -6
- package/dist/accessi-module/Services/UserService/UserService.js +110 -84
- package/dist/accessi-module/Services/UserService/UserService.js.map +1 -1
- package/dist/accessi-module/Views/reset-password.html +39 -21
- package/dist/accessi-module/index.js +17 -3
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/jwt/jwt.strategy.d.ts +4 -2
- package/dist/accessi-module/jwt/jwt.strategy.js +47 -19
- package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
- package/dist/accessi-module/middleware/authenticateGen.d.ts +3 -1
- package/dist/accessi-module/middleware/authenticateGen.js +17 -11
- package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
- package/dist/accessi-module/security/accessControl.d.ts +17 -0
- package/dist/accessi-module/security/accessControl.js +44 -0
- package/dist/accessi-module/security/accessControl.js.map +1 -0
- package/dist/accessi-module/security/authenticatedToken.d.ts +11 -0
- package/dist/accessi-module/security/authenticatedToken.js +39 -0
- package/dist/accessi-module/security/authenticatedToken.js.map +1 -0
- package/dist/accessi-module/security/passwordResetToken.d.ts +8 -0
- package/dist/accessi-module/security/passwordResetToken.js +73 -0
- package/dist/accessi-module/security/passwordResetToken.js.map +1 -0
- package/dist/accessi-module/security/publicAuthRateLimit.d.ts +10 -0
- package/dist/accessi-module/security/publicAuthRateLimit.js +105 -0
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -0
- package/dist/emilsoftware-utilities-2.0.0-dev.1.tgz +0 -0
- package/package.json +1 -1
- package/dist/emilsoftware-utilities-1.8.0-dev.2.tgz +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticatedToken.js","sourceRoot":"","sources":["../../../src/accessi-module/security/authenticatedToken.ts"],"names":[],"mappings":";;AAkBA,kFAkBC;AAED,wEAsBC;AAED,4EAIC;AAlED,mEAAgE;AAUhE,SAAS,oBAAoB,CAAC,KAAc;IAC1C,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,GAAG,CAAC;AACxD,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC;AAED,SAAgB,mCAAmC,CAAC,OAAgB;;IAClE,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3E,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;QACrC,CAAC,CAAC,OAAO,CAAC,MAAM;QAChB,CAAC,CAAC,QAAQ,CAAC,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,MAAM,CAAC;YAC1B,CAAC,CAAC,QAAQ,CAAC,MAAM;YACjB,CAAC,CAAC,SAAS,CAAC;IAEhB,MAAM,YAAY,GAAG,MAAM,CAAC,MAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,YAAY,mCAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IAC1E,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,YAAY,IAAI,CAAC,EAAE,CAAC;QACxD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAgB,8BAA8B,CAC5C,OAAgB,EAChB,MAAwC;IAExC,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,mBAAM,OAAO,EAAG,CAAC,CAAC,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,mBAAM,WAAW,CAAC,QAAQ,EAAG,CAAC,CAAC,EAAE,CAAC;IAEnF,MAAM,cAAc,mCACf,MAAM,KACT,SAAS,EAAE,oBAAoB,CAAC,MAAM,CAAC,SAAS,CAAC,EACjD,qBAAqB,EAAE,oBAAoB,CAAC,MAAM,CAAC,qBAAqB,CAAC,GAC1E,CAAC;IAEF,uCACK,WAAW,KACd,MAAM,EAAE,cAAc,EACtB,QAAQ,kCACH,QAAQ,KACX,MAAM,EAAE,cAAc,KAExB,YAAY,EAAE,OAAO,IACrB;AACJ,CAAC;AAED,SAAgB,gCAAgC,CAC9C,MAA2D;IAE3D,OAAO,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,kBAAkB,MAAK,uCAAkB,CAAC,IAAI,CAAC;AAChE,CAAC","sourcesContent":["import { StatoRegistrazione } from '../Dtos/StatoRegistrazione';\n\nexport interface AccessiAuthenticatedUserSnapshot {\n codiceUtente: number;\n email?: string;\n statoRegistrazione: StatoRegistrazione;\n flagSuper: boolean;\n flagAdminConfigurator: boolean;\n}\n\nfunction normalizeBooleanFlag(value: unknown): boolean {\n return value === true || value === 1 || value === '1';\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport function resolveCodiceUtenteFromTokenPayload(decoded: unknown): number | undefined {\n if (!isRecord(decoded)) {\n return undefined;\n }\n\n const userData = isRecord(decoded.userData) ? decoded.userData : undefined;\n const utente = isRecord(decoded.utente)\n ? decoded.utente\n : isRecord(userData?.utente)\n ? userData.utente\n : undefined;\n\n const codiceUtente = Number(utente?.codiceUtente ?? decoded.codiceUtente);\n if (!Number.isFinite(codiceUtente) || codiceUtente <= 0) {\n return undefined;\n }\n\n return codiceUtente;\n}\n\nexport function buildAuthenticatedTokenPayload(\n decoded: unknown,\n utente: AccessiAuthenticatedUserSnapshot,\n): Record<string, unknown> {\n const basePayload = isRecord(decoded) ? { ...decoded } : {};\n const userData = isRecord(basePayload.userData) ? { ...basePayload.userData } : {};\n\n const normalizedUser = {\n ...utente,\n flagSuper: normalizeBooleanFlag(utente.flagSuper),\n flagAdminConfigurator: normalizeBooleanFlag(utente.flagAdminConfigurator),\n };\n\n return {\n ...basePayload,\n utente: normalizedUser,\n userData: {\n ...userData,\n utente: normalizedUser,\n },\n tokenPayload: decoded,\n };\n}\n\nexport function isAuthenticatedUserEnabledForJwt(\n utente: AccessiAuthenticatedUserSnapshot | null | undefined,\n): boolean {\n return utente?.statoRegistrazione === StatoRegistrazione.CONF;\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AccessiOptions } from '../AccessiModule';
|
|
2
|
+
export declare const DEFAULT_PASSWORD_RESET_EXPIRES_IN = "1h";
|
|
3
|
+
export declare function getAccessiJwtSecret(options: AccessiOptions): string;
|
|
4
|
+
export declare function createPasswordResetToken(codiceUtente: number, nonce: string, secret: string, expiresIn?: string): string;
|
|
5
|
+
export declare function verifyPasswordResetToken(token: string, secret: string): {
|
|
6
|
+
codiceUtente: number;
|
|
7
|
+
nonce: string;
|
|
8
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_PASSWORD_RESET_EXPIRES_IN = void 0;
|
|
37
|
+
exports.getAccessiJwtSecret = getAccessiJwtSecret;
|
|
38
|
+
exports.createPasswordResetToken = createPasswordResetToken;
|
|
39
|
+
exports.verifyPasswordResetToken = verifyPasswordResetToken;
|
|
40
|
+
const jwt = __importStar(require("jsonwebtoken"));
|
|
41
|
+
exports.DEFAULT_PASSWORD_RESET_EXPIRES_IN = '1h';
|
|
42
|
+
function getAccessiJwtSecret(options) {
|
|
43
|
+
var _a;
|
|
44
|
+
const secret = ((_a = options === null || options === void 0 ? void 0 : options.jwtOptions) === null || _a === void 0 ? void 0 : _a.secret) || process.env.ACC_JWT_SECRET;
|
|
45
|
+
if (!secret) {
|
|
46
|
+
throw new Error('JWT secret non configurato.');
|
|
47
|
+
}
|
|
48
|
+
return secret;
|
|
49
|
+
}
|
|
50
|
+
function createPasswordResetToken(codiceUtente, nonce, secret, expiresIn = exports.DEFAULT_PASSWORD_RESET_EXPIRES_IN) {
|
|
51
|
+
if (!codiceUtente || !nonce) {
|
|
52
|
+
throw new Error('Dati insufficienti per generare il reset token.');
|
|
53
|
+
}
|
|
54
|
+
return jwt.sign({
|
|
55
|
+
nonce,
|
|
56
|
+
typ: 'password-reset',
|
|
57
|
+
}, secret, {
|
|
58
|
+
subject: `${codiceUtente}`,
|
|
59
|
+
expiresIn: expiresIn,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function verifyPasswordResetToken(token, secret) {
|
|
63
|
+
const decoded = jwt.verify(token, secret);
|
|
64
|
+
const codiceUtente = Number(decoded === null || decoded === void 0 ? void 0 : decoded.sub);
|
|
65
|
+
if ((decoded === null || decoded === void 0 ? void 0 : decoded.typ) !== 'password-reset' || !codiceUtente || typeof (decoded === null || decoded === void 0 ? void 0 : decoded.nonce) !== 'string') {
|
|
66
|
+
throw new Error('Token di reset non valido.');
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
codiceUtente,
|
|
70
|
+
nonce: decoded.nonce,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=passwordResetToken.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passwordResetToken.js","sourceRoot":"","sources":["../../../src/accessi-module/security/passwordResetToken.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA,kDAOC;AAED,4DAqBC;AAED,4DAeC;AAzDD,kDAAoC;AAGvB,QAAA,iCAAiC,GAAG,IAAI,CAAC;AAOtD,SAAgB,mBAAmB,CAAC,OAAuB;;IACzD,MAAM,MAAM,GAAG,CAAA,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU,0CAAE,MAAM,KAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACzE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAgB,wBAAwB,CACtC,YAAoB,EACpB,KAAa,EACb,MAAc,EACd,YAAoB,yCAAiC;IAErD,IAAI,CAAC,YAAY,IAAI,CAAC,KAAK,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CACb;QACE,KAAK;QACL,GAAG,EAAE,gBAAgB;KACtB,EACD,MAAM,EACN;QACE,OAAO,EAAE,GAAG,YAAY,EAAE;QAC1B,SAAS,EAAE,SAAgB;KAC5B,CACF,CAAC;AACJ,CAAC;AAED,SAAgB,wBAAwB,CACtC,KAAa,EACb,MAAc;IAEd,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAA8B,CAAC;IACvE,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG,CAAC,CAAC;IAE1C,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG,MAAK,gBAAgB,IAAI,CAAC,YAAY,IAAI,OAAO,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,KAAK,CAAA,KAAK,QAAQ,EAAE,CAAC;QAC7F,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAChD,CAAC;IAED,OAAO;QACL,YAAY;QACZ,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC;AACJ,CAAC","sourcesContent":["import * as jwt from 'jsonwebtoken';\nimport { AccessiOptions } from '../AccessiModule';\n\nexport const DEFAULT_PASSWORD_RESET_EXPIRES_IN = '1h';\n\ninterface PasswordResetTokenPayload extends jwt.JwtPayload {\n nonce: string;\n typ: 'password-reset';\n}\n\nexport function getAccessiJwtSecret(options: AccessiOptions): string {\n const secret = options?.jwtOptions?.secret || process.env.ACC_JWT_SECRET;\n if (!secret) {\n throw new Error('JWT secret non configurato.');\n }\n\n return secret;\n}\n\nexport function createPasswordResetToken(\n codiceUtente: number,\n nonce: string,\n secret: string,\n expiresIn: string = DEFAULT_PASSWORD_RESET_EXPIRES_IN,\n): string {\n if (!codiceUtente || !nonce) {\n throw new Error('Dati insufficienti per generare il reset token.');\n }\n\n return jwt.sign(\n {\n nonce,\n typ: 'password-reset',\n },\n secret,\n {\n subject: `${codiceUtente}`,\n expiresIn: expiresIn as any,\n },\n );\n}\n\nexport function verifyPasswordResetToken(\n token: string,\n secret: string,\n): { codiceUtente: number; nonce: string } {\n const decoded = jwt.verify(token, secret) as PasswordResetTokenPayload;\n const codiceUtente = Number(decoded?.sub);\n\n if (decoded?.typ !== 'password-reset' || !codiceUtente || typeof decoded?.nonce !== 'string') {\n throw new Error('Token di reset non valido.');\n }\n\n return {\n codiceUtente,\n nonce: decoded.nonce,\n };\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import { AccessiOptions } from '../AccessiModule';
|
|
3
|
+
export type PublicAuthRateLimitScope = 'login' | 'register' | 'passwordResetEmail' | 'passwordResetConfirm' | 'getUserByToken';
|
|
4
|
+
type RateLimitDecision = {
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
retryAfterSeconds?: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function checkPublicAuthRateLimit(options: AccessiOptions, scope: PublicAuthRateLimitScope, req: Request, identifiers?: string[]): RateLimitDecision;
|
|
9
|
+
export declare function sendPublicAuthRateLimitExceeded(res: Response, retryAfterSeconds: number): Response;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkPublicAuthRateLimit = checkPublicAuthRateLimit;
|
|
4
|
+
exports.sendPublicAuthRateLimitExceeded = sendPublicAuthRateLimitExceeded;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const Utilities_1 = require("../../Utilities");
|
|
7
|
+
const DEFAULT_PUBLIC_AUTH_RATE_LIMITS = {
|
|
8
|
+
login: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },
|
|
9
|
+
register: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },
|
|
10
|
+
passwordResetEmail: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },
|
|
11
|
+
passwordResetConfirm: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },
|
|
12
|
+
getUserByToken: { windowMs: 5 * 60 * 1000, maxAttempts: 30 },
|
|
13
|
+
};
|
|
14
|
+
const RATE_LIMIT_BUCKETS = new Map();
|
|
15
|
+
const MAX_TRACKED_BUCKETS = 20000;
|
|
16
|
+
const MAX_WINDOW_MS = Math.max(...Object.values(DEFAULT_PUBLIC_AUTH_RATE_LIMITS).map((rule) => rule.windowMs));
|
|
17
|
+
function getRule(options, scope) {
|
|
18
|
+
var _a;
|
|
19
|
+
const configuredRule = (_a = options.publicAuthRateLimit) === null || _a === void 0 ? void 0 : _a[scope];
|
|
20
|
+
if (configuredRule &&
|
|
21
|
+
Number.isFinite(configuredRule.windowMs) &&
|
|
22
|
+
configuredRule.windowMs > 0 &&
|
|
23
|
+
Number.isFinite(configuredRule.maxAttempts) &&
|
|
24
|
+
configuredRule.maxAttempts > 0) {
|
|
25
|
+
return {
|
|
26
|
+
windowMs: Math.trunc(configuredRule.windowMs),
|
|
27
|
+
maxAttempts: Math.trunc(configuredRule.maxAttempts),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return DEFAULT_PUBLIC_AUTH_RATE_LIMITS[scope];
|
|
31
|
+
}
|
|
32
|
+
function normalizeIdentifier(identifier) {
|
|
33
|
+
if (typeof identifier !== 'string') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const normalized = identifier.trim().toLowerCase();
|
|
37
|
+
return normalized === '' ? null : normalized;
|
|
38
|
+
}
|
|
39
|
+
function pruneRateLimitBuckets(now) {
|
|
40
|
+
if (RATE_LIMIT_BUCKETS.size < MAX_TRACKED_BUCKETS) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const [key, bucket] of RATE_LIMIT_BUCKETS.entries()) {
|
|
44
|
+
if (bucket.resetAt <= now || bucket.lastSeenAt <= now - MAX_WINDOW_MS * 2) {
|
|
45
|
+
RATE_LIMIT_BUCKETS.delete(key);
|
|
46
|
+
}
|
|
47
|
+
if (RATE_LIMIT_BUCKETS.size < MAX_TRACKED_BUCKETS) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function consumeBucket(bucketKey, rule, now) {
|
|
53
|
+
const existingBucket = RATE_LIMIT_BUCKETS.get(bucketKey);
|
|
54
|
+
if (!existingBucket || existingBucket.resetAt <= now) {
|
|
55
|
+
RATE_LIMIT_BUCKETS.set(bucketKey, {
|
|
56
|
+
count: 1,
|
|
57
|
+
resetAt: now + rule.windowMs,
|
|
58
|
+
lastSeenAt: now,
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
existingBucket.lastSeenAt = now;
|
|
63
|
+
if (existingBucket.count >= rule.maxAttempts) {
|
|
64
|
+
return Math.max(1, Math.ceil((existingBucket.resetAt - now) / 1000));
|
|
65
|
+
}
|
|
66
|
+
existingBucket.count += 1;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function checkPublicAuthRateLimit(options, scope, req, identifiers = []) {
|
|
70
|
+
var _a, _b, _c, _d, _e;
|
|
71
|
+
if (((_a = options.publicAuthRateLimit) === null || _a === void 0 ? void 0 : _a.enabled) === false) {
|
|
72
|
+
return { allowed: true };
|
|
73
|
+
}
|
|
74
|
+
const rule = getRule(options, scope);
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
pruneRateLimitBuckets(now);
|
|
77
|
+
const ipAddress = (_e = normalizeIdentifier((_d = (_b = req.ip) !== null && _b !== void 0 ? _b : (_c = req.socket) === null || _c === void 0 ? void 0 : _c.remoteAddress) !== null && _d !== void 0 ? _d : 'unknown')) !== null && _e !== void 0 ? _e : 'unknown';
|
|
78
|
+
const bucketKeys = new Set([`${scope}|ip|${ipAddress}`]);
|
|
79
|
+
for (const identifier of identifiers) {
|
|
80
|
+
const normalizedIdentifier = normalizeIdentifier(identifier);
|
|
81
|
+
if (!normalizedIdentifier) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
bucketKeys.add(`${scope}|ip-subject|${ipAddress}|${normalizedIdentifier}`);
|
|
85
|
+
}
|
|
86
|
+
let retryAfterSeconds = 0;
|
|
87
|
+
for (const bucketKey of bucketKeys) {
|
|
88
|
+
const bucketRetryAfterSeconds = consumeBucket(bucketKey, rule, now);
|
|
89
|
+
if (bucketRetryAfterSeconds) {
|
|
90
|
+
retryAfterSeconds = Math.max(retryAfterSeconds, bucketRetryAfterSeconds);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (retryAfterSeconds > 0) {
|
|
94
|
+
return {
|
|
95
|
+
allowed: false,
|
|
96
|
+
retryAfterSeconds,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { allowed: true };
|
|
100
|
+
}
|
|
101
|
+
function sendPublicAuthRateLimitExceeded(res, retryAfterSeconds) {
|
|
102
|
+
res.setHeader('Retry-After', String(retryAfterSeconds));
|
|
103
|
+
return Utilities_1.RestUtilities.sendErrorMessage(res, 'Troppi tentativi. Riprova piu tardi.', 'PublicAuthRateLimit', common_1.HttpStatus.TOO_MANY_REQUESTS);
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=publicAuthRateLimit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicAuthRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/publicAuthRateLimit.ts"],"names":[],"mappings":"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","sourcesContent":["import { HttpStatus } from '@nestjs/common';\nimport { Request, Response } from 'express';\nimport { RestUtilities } from '../../Utilities';\nimport { AccessiOptions, PublicAuthRateLimitRuleOptions } from '../AccessiModule';\n\nexport type PublicAuthRateLimitScope =\n | 'login'\n | 'register'\n | 'passwordResetEmail'\n | 'passwordResetConfirm'\n | 'getUserByToken';\n\ntype RateLimitBucket = {\n count: number;\n resetAt: number;\n lastSeenAt: number;\n};\n\ntype RateLimitDecision = {\n allowed: boolean;\n retryAfterSeconds?: number;\n};\n\nconst DEFAULT_PUBLIC_AUTH_RATE_LIMITS: Record<\n PublicAuthRateLimitScope,\n PublicAuthRateLimitRuleOptions\n> = {\n login: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },\n register: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },\n passwordResetEmail: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },\n passwordResetConfirm: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },\n getUserByToken: { windowMs: 5 * 60 * 1000, maxAttempts: 30 },\n};\n\nconst RATE_LIMIT_BUCKETS = new Map<string, RateLimitBucket>();\nconst MAX_TRACKED_BUCKETS = 20000;\nconst MAX_WINDOW_MS = Math.max(\n ...Object.values(DEFAULT_PUBLIC_AUTH_RATE_LIMITS).map((rule) => rule.windowMs),\n);\n\nfunction getRule(\n options: AccessiOptions,\n scope: PublicAuthRateLimitScope,\n): PublicAuthRateLimitRuleOptions {\n const configuredRule = options.publicAuthRateLimit?.[scope];\n\n if (\n configuredRule &&\n Number.isFinite(configuredRule.windowMs) &&\n configuredRule.windowMs > 0 &&\n Number.isFinite(configuredRule.maxAttempts) &&\n configuredRule.maxAttempts > 0\n ) {\n return {\n windowMs: Math.trunc(configuredRule.windowMs),\n maxAttempts: Math.trunc(configuredRule.maxAttempts),\n };\n }\n\n return DEFAULT_PUBLIC_AUTH_RATE_LIMITS[scope];\n}\n\nfunction normalizeIdentifier(identifier: string): string | null {\n if (typeof identifier !== 'string') {\n return null;\n }\n\n const normalized = identifier.trim().toLowerCase();\n return normalized === '' ? null : normalized;\n}\n\nfunction pruneRateLimitBuckets(now: number): void {\n if (RATE_LIMIT_BUCKETS.size < MAX_TRACKED_BUCKETS) {\n return;\n }\n\n for (const [key, bucket] of RATE_LIMIT_BUCKETS.entries()) {\n if (bucket.resetAt <= now || bucket.lastSeenAt <= now - MAX_WINDOW_MS * 2) {\n RATE_LIMIT_BUCKETS.delete(key);\n }\n\n if (RATE_LIMIT_BUCKETS.size < MAX_TRACKED_BUCKETS) {\n return;\n }\n }\n}\n\nfunction consumeBucket(\n bucketKey: string,\n rule: PublicAuthRateLimitRuleOptions,\n now: number,\n): number | null {\n const existingBucket = RATE_LIMIT_BUCKETS.get(bucketKey);\n if (!existingBucket || existingBucket.resetAt <= now) {\n RATE_LIMIT_BUCKETS.set(bucketKey, {\n count: 1,\n resetAt: now + rule.windowMs,\n lastSeenAt: now,\n });\n return null;\n }\n\n existingBucket.lastSeenAt = now;\n if (existingBucket.count >= rule.maxAttempts) {\n return Math.max(1, Math.ceil((existingBucket.resetAt - now) / 1000));\n }\n\n existingBucket.count += 1;\n return null;\n}\n\nexport function checkPublicAuthRateLimit(\n options: AccessiOptions,\n scope: PublicAuthRateLimitScope,\n req: Request,\n identifiers: string[] = [],\n): RateLimitDecision {\n if (options.publicAuthRateLimit?.enabled === false) {\n return { allowed: true };\n }\n\n const rule = getRule(options, scope);\n const now = Date.now();\n pruneRateLimitBuckets(now);\n\n const ipAddress =\n normalizeIdentifier(req.ip ?? req.socket?.remoteAddress ?? 'unknown') ?? 'unknown';\n const bucketKeys = new Set<string>([`${scope}|ip|${ipAddress}`]);\n\n for (const identifier of identifiers) {\n const normalizedIdentifier = normalizeIdentifier(identifier);\n if (!normalizedIdentifier) {\n continue;\n }\n\n bucketKeys.add(`${scope}|ip-subject|${ipAddress}|${normalizedIdentifier}`);\n }\n\n let retryAfterSeconds = 0;\n for (const bucketKey of bucketKeys) {\n const bucketRetryAfterSeconds = consumeBucket(bucketKey, rule, now);\n if (bucketRetryAfterSeconds) {\n retryAfterSeconds = Math.max(retryAfterSeconds, bucketRetryAfterSeconds);\n }\n }\n\n if (retryAfterSeconds > 0) {\n return {\n allowed: false,\n retryAfterSeconds,\n };\n }\n\n return { allowed: true };\n}\n\nexport function sendPublicAuthRateLimitExceeded(\n res: Response,\n retryAfterSeconds: number,\n): Response {\n res.setHeader('Retry-After', String(retryAfterSeconds));\n return RestUtilities.sendErrorMessage(\n res,\n 'Troppi tentativi. Riprova piu tardi.',\n 'PublicAuthRateLimit',\n HttpStatus.TOO_MANY_REQUESTS,\n );\n}\n"]}
|
|
Binary file
|
package/package.json
CHANGED
|
Binary file
|