@platform-modules/civil-aviation-authority 2.3.301 → 2.3.303
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/auth/authenticate-jwt-session.d.ts +19 -0
- package/dist/auth/authenticate-jwt-session.js +27 -0
- package/dist/auth/jwt-token.helper.d.ts +18 -0
- package/dist/auth/jwt-token.helper.js +58 -0
- package/dist/auth/user-session-validation.d.ts +26 -0
- package/dist/auth/user-session-validation.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/models/ResidentialUnitRentalRequestModel.d.ts +1 -0
- package/dist/models/ResidentialUnitRentalRequestModel.js +4 -0
- package/package.json +1 -1
- package/sql/caa_api_payload_changes_2026_07.sql +3 -0
- package/src/auth/authenticate-jwt-session.ts +51 -0
- package/src/auth/jwt-token.helper.ts +71 -0
- package/src/auth/user-session-validation.ts +78 -0
- package/src/index.ts +3 -0
- package/src/models/ResidentialUnitRentalRequestModel.ts +2 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { UserSessionReader } from './user-session-validation';
|
|
2
|
+
export type JwtAuthFailure = {
|
|
3
|
+
ok: false;
|
|
4
|
+
statusCode: 401;
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
export type JwtAuthSuccess = {
|
|
8
|
+
ok: true;
|
|
9
|
+
claims: Record<string, unknown>;
|
|
10
|
+
userId: number;
|
|
11
|
+
sessionId: number | null;
|
|
12
|
+
};
|
|
13
|
+
export type JwtAuthResult = JwtAuthSuccess | JwtAuthFailure;
|
|
14
|
+
/**
|
|
15
|
+
* After jwt.verify(), enforce server-side session binding (logout revocation).
|
|
16
|
+
*/
|
|
17
|
+
export declare function authorizeVerifiedJwtSession(verified: Record<string, unknown>, sessionReader: UserSessionReader, options?: {
|
|
18
|
+
requireActiveSession?: boolean;
|
|
19
|
+
}): Promise<JwtAuthResult>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authorizeVerifiedJwtSession = authorizeVerifiedJwtSession;
|
|
4
|
+
const jwt_token_helper_1 = require("./jwt-token.helper");
|
|
5
|
+
const user_session_validation_1 = require("./user-session-validation");
|
|
6
|
+
/**
|
|
7
|
+
* After jwt.verify(), enforce server-side session binding (logout revocation).
|
|
8
|
+
*/
|
|
9
|
+
async function authorizeVerifiedJwtSession(verified, sessionReader, options) {
|
|
10
|
+
const parsed = (0, jwt_token_helper_1.parseVerifiedJwtClaims)(verified);
|
|
11
|
+
if (!parsed) {
|
|
12
|
+
return { ok: false, statusCode: 401, message: 'Invalid Token' };
|
|
13
|
+
}
|
|
14
|
+
const requireActiveSession = options?.requireActiveSession !== false;
|
|
15
|
+
if (requireActiveSession) {
|
|
16
|
+
const sessionValid = await (0, user_session_validation_1.validateUserSessionBinding)(parsed.userId, parsed.sessionId, sessionReader);
|
|
17
|
+
if (!sessionValid) {
|
|
18
|
+
return { ok: false, statusCode: 401, message: 'Session Expired please login again' };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
claims: parsed.raw,
|
|
24
|
+
userId: parsed.userId,
|
|
25
|
+
sessionId: parsed.sessionId,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract CAA API JWT from standard request headers.
|
|
3
|
+
* Supports `jwt` header (legacy) and `Authorization: Bearer <token>`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function extractJwtToken(headers: Record<string, string | string[] | undefined> | undefined): string | null;
|
|
6
|
+
export type JwtTokenRequest = {
|
|
7
|
+
headers: Record<string, string | string[] | undefined>;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
};
|
|
10
|
+
/** JWT from headers (`jwt`, `Authorization: Bearer`) or JSON body (`token`, `jwt`, `access_token`). */
|
|
11
|
+
export declare function extractJwtTokenFromRequest(request: JwtTokenRequest): string | null;
|
|
12
|
+
export type VerifiedJwtClaims = {
|
|
13
|
+
userId: number;
|
|
14
|
+
sessionId: number | null;
|
|
15
|
+
raw: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
/** Parse verified JWT payload into userId + sessionId. */
|
|
18
|
+
export declare function parseVerifiedJwtClaims(verified: Record<string, unknown>): VerifiedJwtClaims | null;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractJwtToken = extractJwtToken;
|
|
4
|
+
exports.extractJwtTokenFromRequest = extractJwtTokenFromRequest;
|
|
5
|
+
exports.parseVerifiedJwtClaims = parseVerifiedJwtClaims;
|
|
6
|
+
/**
|
|
7
|
+
* Extract CAA API JWT from standard request headers.
|
|
8
|
+
* Supports `jwt` header (legacy) and `Authorization: Bearer <token>`.
|
|
9
|
+
*/
|
|
10
|
+
function extractJwtToken(headers) {
|
|
11
|
+
if (!headers)
|
|
12
|
+
return null;
|
|
13
|
+
const pick = (value) => {
|
|
14
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
15
|
+
const trimmed = raw?.trim();
|
|
16
|
+
return trimmed ? trimmed : null;
|
|
17
|
+
};
|
|
18
|
+
const fromJwtHeader = pick(headers.jwt ?? headers.JWT);
|
|
19
|
+
if (fromJwtHeader)
|
|
20
|
+
return fromJwtHeader;
|
|
21
|
+
const authorization = pick(headers.authorization ?? headers.Authorization);
|
|
22
|
+
if (!authorization)
|
|
23
|
+
return null;
|
|
24
|
+
const bearerMatch = /^Bearer\s+(.+)$/i.exec(authorization);
|
|
25
|
+
if (bearerMatch?.[1]?.trim())
|
|
26
|
+
return bearerMatch[1].trim();
|
|
27
|
+
return authorization;
|
|
28
|
+
}
|
|
29
|
+
/** JWT from headers (`jwt`, `Authorization: Bearer`) or JSON body (`token`, `jwt`, `access_token`). */
|
|
30
|
+
function extractJwtTokenFromRequest(request) {
|
|
31
|
+
const fromHeaders = extractJwtToken(request.headers);
|
|
32
|
+
if (fromHeaders)
|
|
33
|
+
return fromHeaders;
|
|
34
|
+
const body = request.body;
|
|
35
|
+
if (!body || typeof body !== 'object')
|
|
36
|
+
return null;
|
|
37
|
+
for (const key of ['token', 'jwt', 'access_token']) {
|
|
38
|
+
const value = body[key];
|
|
39
|
+
if (typeof value === 'string' && value.trim())
|
|
40
|
+
return value.trim();
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/** Parse verified JWT payload into userId + sessionId. */
|
|
45
|
+
function parseVerifiedJwtClaims(verified) {
|
|
46
|
+
const userId = Number(verified.userId);
|
|
47
|
+
if (!Number.isFinite(userId) || userId <= 0)
|
|
48
|
+
return null;
|
|
49
|
+
const sessionIdRaw = verified.sessionId;
|
|
50
|
+
const parsedSessionId = sessionIdRaw == null || sessionIdRaw === '' ? null : Number(sessionIdRaw);
|
|
51
|
+
return {
|
|
52
|
+
userId,
|
|
53
|
+
sessionId: parsedSessionId != null && Number.isFinite(parsedSessionId) && parsedSessionId > 0
|
|
54
|
+
? parsedSessionId
|
|
55
|
+
: null,
|
|
56
|
+
raw: verified,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side JWT session binding (CAA VAPT M003/M004).
|
|
3
|
+
* JWT remains valid cryptographically until exp; access is allowed only while
|
|
4
|
+
* the bound user_sessions row is active and not expired.
|
|
5
|
+
*/
|
|
6
|
+
export type UserSessionReader = {
|
|
7
|
+
isSessionActiveById(sessionId: number, userId: number): Promise<boolean>;
|
|
8
|
+
/** Legacy fallback when REQUIRE_JWT_SESSION_ID=false and token has no sessionId. */
|
|
9
|
+
isSessionActive?(userId: number): Promise<unknown>;
|
|
10
|
+
};
|
|
11
|
+
/** When true (default), JWT must include sessionId and that session must be active. */
|
|
12
|
+
export declare function isRequireJwtSessionId(): boolean;
|
|
13
|
+
export declare function resolveJwtSessionId(sessionId: unknown): number | null;
|
|
14
|
+
/**
|
|
15
|
+
* Validate that a verified JWT is still authorized (not logged out / expired server-side).
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateUserSessionBinding(userId: number, sessionId: number | null | undefined, reader: UserSessionReader): Promise<boolean>;
|
|
18
|
+
/** Shared TypeORM filters: active, not deleted, not past expires_at. */
|
|
19
|
+
export declare function applyActiveUserSessionFilters(qb: {
|
|
20
|
+
andWhere: (clause: string, params?: Record<string, unknown>) => unknown;
|
|
21
|
+
}, alias?: string): void;
|
|
22
|
+
/** @deprecated Use applyActiveUserSessionFilters — kept for backward compatibility. */
|
|
23
|
+
export declare function activeUserSessionSql(alias?: string): {
|
|
24
|
+
clause: string;
|
|
25
|
+
params: Record<string, unknown>;
|
|
26
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Server-side JWT session binding (CAA VAPT M003/M004).
|
|
4
|
+
* JWT remains valid cryptographically until exp; access is allowed only while
|
|
5
|
+
* the bound user_sessions row is active and not expired.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.isRequireJwtSessionId = isRequireJwtSessionId;
|
|
9
|
+
exports.resolveJwtSessionId = resolveJwtSessionId;
|
|
10
|
+
exports.validateUserSessionBinding = validateUserSessionBinding;
|
|
11
|
+
exports.applyActiveUserSessionFilters = applyActiveUserSessionFilters;
|
|
12
|
+
exports.activeUserSessionSql = activeUserSessionSql;
|
|
13
|
+
/** When true (default), JWT must include sessionId and that session must be active. */
|
|
14
|
+
function isRequireJwtSessionId() {
|
|
15
|
+
const raw = (process.env.REQUIRE_JWT_SESSION_ID ?? 'true').trim().toLowerCase();
|
|
16
|
+
return raw !== 'false' && raw !== '0' && raw !== 'no';
|
|
17
|
+
}
|
|
18
|
+
function resolveJwtSessionId(sessionId) {
|
|
19
|
+
if (sessionId == null || sessionId === '')
|
|
20
|
+
return null;
|
|
21
|
+
const sid = Number(sessionId);
|
|
22
|
+
if (!Number.isFinite(sid) || sid <= 0)
|
|
23
|
+
return null;
|
|
24
|
+
return sid;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate that a verified JWT is still authorized (not logged out / expired server-side).
|
|
28
|
+
*/
|
|
29
|
+
async function validateUserSessionBinding(userId, sessionId, reader) {
|
|
30
|
+
if (!Number.isFinite(userId) || userId <= 0)
|
|
31
|
+
return false;
|
|
32
|
+
const sid = resolveJwtSessionId(sessionId);
|
|
33
|
+
if (sid != null) {
|
|
34
|
+
return reader.isSessionActiveById(sid, userId);
|
|
35
|
+
}
|
|
36
|
+
if (isRequireJwtSessionId()) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (!reader.isSessionActive)
|
|
40
|
+
return false;
|
|
41
|
+
const legacy = await reader.isSessionActive(userId);
|
|
42
|
+
return !!legacy;
|
|
43
|
+
}
|
|
44
|
+
/** Shared TypeORM filters: active, not deleted, not past expires_at. */
|
|
45
|
+
function applyActiveUserSessionFilters(qb, alias = 'user_sessions') {
|
|
46
|
+
qb.andWhere(`${alias}.is_active = :is_active`, { is_active: true });
|
|
47
|
+
qb.andWhere(`${alias}.is_deleted = :is_deleted`, { is_deleted: false });
|
|
48
|
+
qb.andWhere(`(${alias}.expires_at IS NULL OR ${alias}.expires_at > :session_now)`, { session_now: new Date() });
|
|
49
|
+
}
|
|
50
|
+
/** @deprecated Use applyActiveUserSessionFilters — kept for backward compatibility. */
|
|
51
|
+
function activeUserSessionSql(alias = 'user_sessions') {
|
|
52
|
+
return {
|
|
53
|
+
clause: `${alias}.is_active = :is_active AND ${alias}.is_deleted = :is_deleted AND (${alias}.expires_at IS NULL OR ${alias}.expires_at > :session_now)`,
|
|
54
|
+
params: {
|
|
55
|
+
is_active: true,
|
|
56
|
+
is_deleted: false,
|
|
57
|
+
session_now: new Date(),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export * from './models/user';
|
|
2
2
|
export * from './models/role';
|
|
3
3
|
export * from './models/user-sessions';
|
|
4
|
+
export * from './auth/user-session-validation';
|
|
5
|
+
export * from './auth/jwt-token.helper';
|
|
6
|
+
export * from './auth/authenticate-jwt-session';
|
|
4
7
|
export * from './models/ITHelpDeskModel';
|
|
5
8
|
export * from './models/ServiceTypeModel';
|
|
6
9
|
export * from './models/CAAServices';
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,9 @@ exports.RespondToEnquiriesRequestAttachment = exports.RespondToEnquiriesWorkFlow
|
|
|
20
20
|
__exportStar(require("./models/user"), exports);
|
|
21
21
|
__exportStar(require("./models/role"), exports);
|
|
22
22
|
__exportStar(require("./models/user-sessions"), exports);
|
|
23
|
+
__exportStar(require("./auth/user-session-validation"), exports);
|
|
24
|
+
__exportStar(require("./auth/jwt-token.helper"), exports);
|
|
25
|
+
__exportStar(require("./auth/authenticate-jwt-session"), exports);
|
|
23
26
|
__exportStar(require("./models/ITHelpDeskModel"), exports);
|
|
24
27
|
// export * from './models/ITServicesTypesSalalahModel'; // REMOVED - table no longer used
|
|
25
28
|
// export * from './models/ITServicesTypesMuscatModel'; // REMOVED - table no longer used
|
|
@@ -69,6 +69,10 @@ __decorate([
|
|
|
69
69
|
(0, typeorm_1.Column)({ type: 'varchar', length: 255, nullable: true }),
|
|
70
70
|
__metadata("design:type", String)
|
|
71
71
|
], ResidentialUnitRentalRequest.prototype, "location_of_unit", void 0);
|
|
72
|
+
__decorate([
|
|
73
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 255, nullable: true }),
|
|
74
|
+
__metadata("design:type", Object)
|
|
75
|
+
], ResidentialUnitRentalRequest.prototype, "number_of_apartments", void 0);
|
|
72
76
|
__decorate([
|
|
73
77
|
(0, typeorm_1.Column)({ type: 'text', nullable: true }),
|
|
74
78
|
__metadata("design:type", String)
|
package/package.json
CHANGED
|
@@ -9,6 +9,9 @@ ALTER TYPE asset_maintenance_category_en ADD VALUE IF NOT EXISTS 'Other';
|
|
|
9
9
|
ALTER TABLE residential_unit_rental_requests
|
|
10
10
|
ALTER COLUMN preferred_start_date DROP NOT NULL;
|
|
11
11
|
|
|
12
|
+
ALTER TABLE residential_unit_rental_requests
|
|
13
|
+
ADD COLUMN IF NOT EXISTS number_of_apartments VARCHAR(255);
|
|
14
|
+
|
|
12
15
|
-- 3. Appeal against administrative decision: optional / removed payload fields
|
|
13
16
|
ALTER TABLE appeal_against_administrative_decision_requests
|
|
14
17
|
ALTER COLUMN description DROP NOT NULL,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { parseVerifiedJwtClaims } from './jwt-token.helper';
|
|
2
|
+
import { validateUserSessionBinding } from './user-session-validation';
|
|
3
|
+
import type { UserSessionReader } from './user-session-validation';
|
|
4
|
+
|
|
5
|
+
export type JwtAuthFailure = {
|
|
6
|
+
ok: false;
|
|
7
|
+
statusCode: 401;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type JwtAuthSuccess = {
|
|
12
|
+
ok: true;
|
|
13
|
+
claims: Record<string, unknown>;
|
|
14
|
+
userId: number;
|
|
15
|
+
sessionId: number | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type JwtAuthResult = JwtAuthSuccess | JwtAuthFailure;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* After jwt.verify(), enforce server-side session binding (logout revocation).
|
|
22
|
+
*/
|
|
23
|
+
export async function authorizeVerifiedJwtSession(
|
|
24
|
+
verified: Record<string, unknown>,
|
|
25
|
+
sessionReader: UserSessionReader,
|
|
26
|
+
options?: { requireActiveSession?: boolean },
|
|
27
|
+
): Promise<JwtAuthResult> {
|
|
28
|
+
const parsed = parseVerifiedJwtClaims(verified);
|
|
29
|
+
if (!parsed) {
|
|
30
|
+
return { ok: false, statusCode: 401, message: 'Invalid Token' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const requireActiveSession = options?.requireActiveSession !== false;
|
|
34
|
+
if (requireActiveSession) {
|
|
35
|
+
const sessionValid = await validateUserSessionBinding(
|
|
36
|
+
parsed.userId,
|
|
37
|
+
parsed.sessionId,
|
|
38
|
+
sessionReader,
|
|
39
|
+
);
|
|
40
|
+
if (!sessionValid) {
|
|
41
|
+
return { ok: false, statusCode: 401, message: 'Session Expired please login again' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
claims: parsed.raw,
|
|
48
|
+
userId: parsed.userId,
|
|
49
|
+
sessionId: parsed.sessionId,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract CAA API JWT from standard request headers.
|
|
3
|
+
* Supports `jwt` header (legacy) and `Authorization: Bearer <token>`.
|
|
4
|
+
*/
|
|
5
|
+
export function extractJwtToken(
|
|
6
|
+
headers: Record<string, string | string[] | undefined> | undefined,
|
|
7
|
+
): string | null {
|
|
8
|
+
if (!headers) return null;
|
|
9
|
+
|
|
10
|
+
const pick = (value: string | string[] | undefined): string | null => {
|
|
11
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
12
|
+
const trimmed = raw?.trim();
|
|
13
|
+
return trimmed ? trimmed : null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const fromJwtHeader = pick(headers.jwt ?? headers.JWT);
|
|
17
|
+
if (fromJwtHeader) return fromJwtHeader;
|
|
18
|
+
|
|
19
|
+
const authorization = pick(headers.authorization ?? headers.Authorization);
|
|
20
|
+
if (!authorization) return null;
|
|
21
|
+
|
|
22
|
+
const bearerMatch = /^Bearer\s+(.+)$/i.exec(authorization);
|
|
23
|
+
if (bearerMatch?.[1]?.trim()) return bearerMatch[1].trim();
|
|
24
|
+
|
|
25
|
+
return authorization;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type JwtTokenRequest = {
|
|
29
|
+
headers: Record<string, string | string[] | undefined>;
|
|
30
|
+
body?: unknown;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** JWT from headers (`jwt`, `Authorization: Bearer`) or JSON body (`token`, `jwt`, `access_token`). */
|
|
34
|
+
export function extractJwtTokenFromRequest(request: JwtTokenRequest): string | null {
|
|
35
|
+
const fromHeaders = extractJwtToken(request.headers);
|
|
36
|
+
if (fromHeaders) return fromHeaders;
|
|
37
|
+
|
|
38
|
+
const body = request.body as Record<string, unknown> | undefined;
|
|
39
|
+
if (!body || typeof body !== 'object') return null;
|
|
40
|
+
|
|
41
|
+
for (const key of ['token', 'jwt', 'access_token'] as const) {
|
|
42
|
+
const value = body[key];
|
|
43
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type VerifiedJwtClaims = {
|
|
49
|
+
userId: number;
|
|
50
|
+
sessionId: number | null;
|
|
51
|
+
raw: Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Parse verified JWT payload into userId + sessionId. */
|
|
55
|
+
export function parseVerifiedJwtClaims(verified: Record<string, unknown>): VerifiedJwtClaims | null {
|
|
56
|
+
const userId = Number(verified.userId);
|
|
57
|
+
if (!Number.isFinite(userId) || userId <= 0) return null;
|
|
58
|
+
|
|
59
|
+
const sessionIdRaw = verified.sessionId;
|
|
60
|
+
const parsedSessionId =
|
|
61
|
+
sessionIdRaw == null || sessionIdRaw === '' ? null : Number(sessionIdRaw);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
userId,
|
|
65
|
+
sessionId:
|
|
66
|
+
parsedSessionId != null && Number.isFinite(parsedSessionId) && parsedSessionId > 0
|
|
67
|
+
? parsedSessionId
|
|
68
|
+
: null,
|
|
69
|
+
raw: verified,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side JWT session binding (CAA VAPT M003/M004).
|
|
3
|
+
* JWT remains valid cryptographically until exp; access is allowed only while
|
|
4
|
+
* the bound user_sessions row is active and not expired.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type UserSessionReader = {
|
|
8
|
+
isSessionActiveById(sessionId: number, userId: number): Promise<boolean>;
|
|
9
|
+
/** Legacy fallback when REQUIRE_JWT_SESSION_ID=false and token has no sessionId. */
|
|
10
|
+
isSessionActive?(userId: number): Promise<unknown>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** When true (default), JWT must include sessionId and that session must be active. */
|
|
14
|
+
export function isRequireJwtSessionId(): boolean {
|
|
15
|
+
const raw = (process.env.REQUIRE_JWT_SESSION_ID ?? 'true').trim().toLowerCase();
|
|
16
|
+
return raw !== 'false' && raw !== '0' && raw !== 'no';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveJwtSessionId(sessionId: unknown): number | null {
|
|
20
|
+
if (sessionId == null || sessionId === '') return null;
|
|
21
|
+
const sid = Number(sessionId);
|
|
22
|
+
if (!Number.isFinite(sid) || sid <= 0) return null;
|
|
23
|
+
return sid;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate that a verified JWT is still authorized (not logged out / expired server-side).
|
|
28
|
+
*/
|
|
29
|
+
export async function validateUserSessionBinding(
|
|
30
|
+
userId: number,
|
|
31
|
+
sessionId: number | null | undefined,
|
|
32
|
+
reader: UserSessionReader,
|
|
33
|
+
): Promise<boolean> {
|
|
34
|
+
if (!Number.isFinite(userId) || userId <= 0) return false;
|
|
35
|
+
|
|
36
|
+
const sid = resolveJwtSessionId(sessionId);
|
|
37
|
+
if (sid != null) {
|
|
38
|
+
return reader.isSessionActiveById(sid, userId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isRequireJwtSessionId()) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!reader.isSessionActive) return false;
|
|
46
|
+
const legacy = await reader.isSessionActive(userId);
|
|
47
|
+
return !!legacy;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Shared TypeORM filters: active, not deleted, not past expires_at. */
|
|
51
|
+
export function applyActiveUserSessionFilters(
|
|
52
|
+
qb: {
|
|
53
|
+
andWhere: (clause: string, params?: Record<string, unknown>) => unknown;
|
|
54
|
+
},
|
|
55
|
+
alias = 'user_sessions',
|
|
56
|
+
): void {
|
|
57
|
+
qb.andWhere(`${alias}.is_active = :is_active`, { is_active: true });
|
|
58
|
+
qb.andWhere(`${alias}.is_deleted = :is_deleted`, { is_deleted: false });
|
|
59
|
+
qb.andWhere(
|
|
60
|
+
`(${alias}.expires_at IS NULL OR ${alias}.expires_at > :session_now)`,
|
|
61
|
+
{ session_now: new Date() },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @deprecated Use applyActiveUserSessionFilters — kept for backward compatibility. */
|
|
66
|
+
export function activeUserSessionSql(alias = 'user_sessions'): {
|
|
67
|
+
clause: string;
|
|
68
|
+
params: Record<string, unknown>;
|
|
69
|
+
} {
|
|
70
|
+
return {
|
|
71
|
+
clause: `${alias}.is_active = :is_active AND ${alias}.is_deleted = :is_deleted AND (${alias}.expires_at IS NULL OR ${alias}.expires_at > :session_now)`,
|
|
72
|
+
params: {
|
|
73
|
+
is_active: true,
|
|
74
|
+
is_deleted: false,
|
|
75
|
+
session_now: new Date(),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { JobTransferRequestStatus } from './models/JobTransferRequestModel';
|
|
|
3
3
|
export * from './models/user';
|
|
4
4
|
export * from './models/role';
|
|
5
5
|
export * from './models/user-sessions';
|
|
6
|
+
export * from './auth/user-session-validation';
|
|
7
|
+
export * from './auth/jwt-token.helper';
|
|
8
|
+
export * from './auth/authenticate-jwt-session';
|
|
6
9
|
export * from './models/ITHelpDeskModel';
|
|
7
10
|
// export * from './models/ITServicesTypesSalalahModel'; // REMOVED - table no longer used
|
|
8
11
|
// export * from './models/ITServicesTypesMuscatModel'; // REMOVED - table no longer used
|
|
@@ -113,8 +113,8 @@ export class ResidentialUnitRentalRequest extends BaseModel {
|
|
|
113
113
|
|
|
114
114
|
location_of_unit: string;
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
@Column({ type: 'varchar', length: 255, nullable: true })
|
|
117
|
+
number_of_apartments: string | null;
|
|
118
118
|
|
|
119
119
|
@Column({ type: 'text', nullable: true })
|
|
120
120
|
|