@fluyappgocore/commons-backend 1.0.211 → 1.0.213
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.
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guest-JWT auth for the customer-facing portal.
|
|
3
|
+
*
|
|
4
|
+
* Three token levels, each signed with the shared JWT_KEY env var so
|
|
5
|
+
* every microservice (moving, video, agents) can verify them without a
|
|
6
|
+
* network round-trip:
|
|
7
|
+
*
|
|
8
|
+
* - guest → "I'm a visitor at entity X, branch Y" (30 min, no ticket)
|
|
9
|
+
* - ticket → "I'm the holder of ticket T at entity X, branch Y" (2 h)
|
|
10
|
+
* - (livekit tokens are minted separately by livekit-server-sdk and
|
|
11
|
+
* are NOT part of this module — they live inside msvideo)
|
|
12
|
+
*
|
|
13
|
+
* Usage in any express app:
|
|
14
|
+
*
|
|
15
|
+
* import { verifyPortalToken, requireScope } from "@fluyappgocore/commons-backend";
|
|
16
|
+
*
|
|
17
|
+
* router.get("/queue",
|
|
18
|
+
* verifyPortalToken,
|
|
19
|
+
* requireScope("guest"), // accepts guest OR ticket
|
|
20
|
+
* handler,
|
|
21
|
+
* );
|
|
22
|
+
*
|
|
23
|
+
* router.get("/tickets/:uuid",
|
|
24
|
+
* verifyPortalToken,
|
|
25
|
+
* requireScope("ticket"),
|
|
26
|
+
* requireTicketMatch("uuid"),
|
|
27
|
+
* handler,
|
|
28
|
+
* );
|
|
29
|
+
*/
|
|
30
|
+
import { NextFunction, Request, Response } from "express";
|
|
31
|
+
export declare type PortalScope = "guest" | "ticket";
|
|
32
|
+
export interface PortalClaims {
|
|
33
|
+
/** "guest:<uuid>" or "ticket:<ticketUuid>" — useful for audit/rate-limit */
|
|
34
|
+
sub: string;
|
|
35
|
+
/** JWT audience, always "portal" so we don't cross-verify with user tokens */
|
|
36
|
+
aud: "portal";
|
|
37
|
+
scope: PortalScope;
|
|
38
|
+
entityUuid: string;
|
|
39
|
+
branchUuid: string;
|
|
40
|
+
/** Present iff scope === "ticket" */
|
|
41
|
+
ticketUuid?: string;
|
|
42
|
+
iat: number;
|
|
43
|
+
exp: number;
|
|
44
|
+
}
|
|
45
|
+
export interface RequestWithPortal extends Request {
|
|
46
|
+
portalAuth?: PortalClaims;
|
|
47
|
+
}
|
|
48
|
+
/** Issue a fresh guest token for a visitor who just landed on a branch URL. */
|
|
49
|
+
export declare function issueGuestToken(input: {
|
|
50
|
+
entityUuid: string;
|
|
51
|
+
branchUuid: string;
|
|
52
|
+
}): {
|
|
53
|
+
token: string;
|
|
54
|
+
expiresAt: number;
|
|
55
|
+
};
|
|
56
|
+
/** Upgrade a guest into a ticket holder after POST /tickets succeeds. */
|
|
57
|
+
export declare function issueTicketToken(input: {
|
|
58
|
+
entityUuid: string;
|
|
59
|
+
branchUuid: string;
|
|
60
|
+
ticketUuid: string;
|
|
61
|
+
}): {
|
|
62
|
+
token: string;
|
|
63
|
+
expiresAt: number;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Express middleware: reads `Authorization: Bearer <token>`, verifies the
|
|
67
|
+
* signature, decodes the claims, attaches them as `req.portalAuth`.
|
|
68
|
+
*
|
|
69
|
+
* Rejects anything that isn't a portal-scoped token (aud !== "portal")
|
|
70
|
+
* so a regular user/agent JWT can NEVER accidentally pass this gate.
|
|
71
|
+
*/
|
|
72
|
+
export declare function verifyPortalToken(req: RequestWithPortal, _res: Response, next: NextFunction): void;
|
|
73
|
+
/**
|
|
74
|
+
* Gate a route by scope.
|
|
75
|
+
*
|
|
76
|
+
* `requireScope("guest")` accepts BOTH guest and ticket tokens (ticket
|
|
77
|
+
* implies guest, since a ticket holder is also a visitor at that branch).
|
|
78
|
+
*
|
|
79
|
+
* `requireScope("ticket")` accepts only ticket tokens.
|
|
80
|
+
*/
|
|
81
|
+
export declare function requireScope(minimum: PortalScope): (req: RequestWithPortal, _res: Response, next: NextFunction) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Guard that the `ticketUuid` claim inside the JWT matches the URL param.
|
|
84
|
+
* Prevents a ticket holder from reading someone else's ticket by tampering
|
|
85
|
+
* with the path.
|
|
86
|
+
*/
|
|
87
|
+
export declare function requireTicketMatch(paramName?: string): (req: RequestWithPortal, _res: Response, next: NextFunction) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Guard that the `branchUuid` claim inside the JWT matches the URL param
|
|
90
|
+
* or body field. Same idea, one layer up.
|
|
91
|
+
*/
|
|
92
|
+
export declare function requireBranchMatch(source: "params" | "body", fieldName: string): (req: RequestWithPortal, _res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
5
|
+
}) : (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
o[k2] = m[k];
|
|
8
|
+
}));
|
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
11
|
+
}) : function(o, v) {
|
|
12
|
+
o["default"] = v;
|
|
13
|
+
});
|
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
15
|
+
if (mod && mod.__esModule) return mod;
|
|
16
|
+
var result = {};
|
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
18
|
+
__setModuleDefault(result, mod);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.requireBranchMatch = exports.requireTicketMatch = exports.requireScope = exports.verifyPortalToken = exports.issueTicketToken = exports.issueGuestToken = void 0;
|
|
23
|
+
var jwt = __importStar(require("jsonwebtoken"));
|
|
24
|
+
var exceptions_1 = require("../exceptions");
|
|
25
|
+
var SECRET = process.env.JWT_KEY || "";
|
|
26
|
+
var GUEST_TTL_SEC = 30 * 60; // 30 minutes
|
|
27
|
+
var TICKET_TTL_SEC = 2 * 60 * 60; // 2 hours
|
|
28
|
+
function randomId() {
|
|
29
|
+
return (Date.now().toString(36) +
|
|
30
|
+
Math.random().toString(36).slice(2, 10));
|
|
31
|
+
}
|
|
32
|
+
/** Issue a fresh guest token for a visitor who just landed on a branch URL. */
|
|
33
|
+
function issueGuestToken(input) {
|
|
34
|
+
var now = Math.floor(Date.now() / 1000);
|
|
35
|
+
var exp = now + GUEST_TTL_SEC;
|
|
36
|
+
var payload = {
|
|
37
|
+
sub: "guest:" + randomId(),
|
|
38
|
+
aud: "portal",
|
|
39
|
+
scope: "guest",
|
|
40
|
+
entityUuid: input.entityUuid,
|
|
41
|
+
branchUuid: input.branchUuid,
|
|
42
|
+
iat: now,
|
|
43
|
+
exp: exp,
|
|
44
|
+
};
|
|
45
|
+
var token = jwt.sign(payload, SECRET, { algorithm: "HS256" });
|
|
46
|
+
return { token: token, expiresAt: exp * 1000 };
|
|
47
|
+
}
|
|
48
|
+
exports.issueGuestToken = issueGuestToken;
|
|
49
|
+
/** Upgrade a guest into a ticket holder after POST /tickets succeeds. */
|
|
50
|
+
function issueTicketToken(input) {
|
|
51
|
+
var now = Math.floor(Date.now() / 1000);
|
|
52
|
+
var exp = now + TICKET_TTL_SEC;
|
|
53
|
+
var payload = {
|
|
54
|
+
sub: "ticket:" + input.ticketUuid,
|
|
55
|
+
aud: "portal",
|
|
56
|
+
scope: "ticket",
|
|
57
|
+
entityUuid: input.entityUuid,
|
|
58
|
+
branchUuid: input.branchUuid,
|
|
59
|
+
ticketUuid: input.ticketUuid,
|
|
60
|
+
iat: now,
|
|
61
|
+
exp: exp,
|
|
62
|
+
};
|
|
63
|
+
var token = jwt.sign(payload, SECRET, { algorithm: "HS256" });
|
|
64
|
+
return { token: token, expiresAt: exp * 1000 };
|
|
65
|
+
}
|
|
66
|
+
exports.issueTicketToken = issueTicketToken;
|
|
67
|
+
/**
|
|
68
|
+
* Express middleware: reads `Authorization: Bearer <token>`, verifies the
|
|
69
|
+
* signature, decodes the claims, attaches them as `req.portalAuth`.
|
|
70
|
+
*
|
|
71
|
+
* Rejects anything that isn't a portal-scoped token (aud !== "portal")
|
|
72
|
+
* so a regular user/agent JWT can NEVER accidentally pass this gate.
|
|
73
|
+
*/
|
|
74
|
+
function verifyPortalToken(req, _res, next) {
|
|
75
|
+
var _a;
|
|
76
|
+
var auth = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.authorization;
|
|
77
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
78
|
+
return next(new exceptions_1.HttpException(401, "Missing portal token"));
|
|
79
|
+
}
|
|
80
|
+
var token = auth.slice("Bearer ".length).trim();
|
|
81
|
+
try {
|
|
82
|
+
var decoded = jwt.verify(token, SECRET, {
|
|
83
|
+
algorithms: ["HS256"],
|
|
84
|
+
});
|
|
85
|
+
if (decoded.aud !== "portal") {
|
|
86
|
+
return next(new exceptions_1.HttpException(401, "Invalid token audience"));
|
|
87
|
+
}
|
|
88
|
+
req.portalAuth = decoded;
|
|
89
|
+
return next();
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
return next(new exceptions_1.HttpException(401, (err === null || err === void 0 ? void 0 : err.name) === "TokenExpiredError"
|
|
93
|
+
? "Portal token expired"
|
|
94
|
+
: "Invalid portal token"));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.verifyPortalToken = verifyPortalToken;
|
|
98
|
+
/**
|
|
99
|
+
* Gate a route by scope.
|
|
100
|
+
*
|
|
101
|
+
* `requireScope("guest")` accepts BOTH guest and ticket tokens (ticket
|
|
102
|
+
* implies guest, since a ticket holder is also a visitor at that branch).
|
|
103
|
+
*
|
|
104
|
+
* `requireScope("ticket")` accepts only ticket tokens.
|
|
105
|
+
*/
|
|
106
|
+
function requireScope(minimum) {
|
|
107
|
+
return function (req, _res, next) {
|
|
108
|
+
if (!req.portalAuth) {
|
|
109
|
+
return next(new exceptions_1.HttpException(401, "Portal token not verified"));
|
|
110
|
+
}
|
|
111
|
+
if (minimum === "ticket" && req.portalAuth.scope !== "ticket") {
|
|
112
|
+
return next(new exceptions_1.HttpException(403, "Ticket-scoped token required"));
|
|
113
|
+
}
|
|
114
|
+
return next();
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
exports.requireScope = requireScope;
|
|
118
|
+
/**
|
|
119
|
+
* Guard that the `ticketUuid` claim inside the JWT matches the URL param.
|
|
120
|
+
* Prevents a ticket holder from reading someone else's ticket by tampering
|
|
121
|
+
* with the path.
|
|
122
|
+
*/
|
|
123
|
+
function requireTicketMatch(paramName) {
|
|
124
|
+
if (paramName === void 0) { paramName = "uuid"; }
|
|
125
|
+
return function (req, _res, next) {
|
|
126
|
+
var _a, _b;
|
|
127
|
+
var claimed = (_a = req.portalAuth) === null || _a === void 0 ? void 0 : _a.ticketUuid;
|
|
128
|
+
var requested = (_b = req.params) === null || _b === void 0 ? void 0 : _b[paramName];
|
|
129
|
+
if (!claimed || !requested || claimed !== requested) {
|
|
130
|
+
return next(new exceptions_1.HttpException(403, "Ticket id mismatch"));
|
|
131
|
+
}
|
|
132
|
+
return next();
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
exports.requireTicketMatch = requireTicketMatch;
|
|
136
|
+
/**
|
|
137
|
+
* Guard that the `branchUuid` claim inside the JWT matches the URL param
|
|
138
|
+
* or body field. Same idea, one layer up.
|
|
139
|
+
*/
|
|
140
|
+
function requireBranchMatch(source, fieldName) {
|
|
141
|
+
return function (req, _res, next) {
|
|
142
|
+
var _a, _b, _c;
|
|
143
|
+
var claimed = (_a = req.portalAuth) === null || _a === void 0 ? void 0 : _a.branchUuid;
|
|
144
|
+
var requested = source === "params" ? (_b = req.params) === null || _b === void 0 ? void 0 : _b[fieldName] : (_c = req.body) === null || _c === void 0 ? void 0 : _c[fieldName];
|
|
145
|
+
if (!claimed || !requested || claimed !== requested) {
|
|
146
|
+
return next(new exceptions_1.HttpException(403, "Branch id mismatch"));
|
|
147
|
+
}
|
|
148
|
+
return next();
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
exports.requireBranchMatch = requireBranchMatch;
|
|
@@ -14,3 +14,4 @@ __exportStar(require("./auth.middleware"), exports);
|
|
|
14
14
|
__exportStar(require("./error.middleware"), exports);
|
|
15
15
|
__exportStar(require("./validation.middleware"), exports);
|
|
16
16
|
__exportStar(require("./licenseGuard.middleware"), exports);
|
|
17
|
+
__exportStar(require("./guestAuth.middleware"), exports);
|
|
@@ -39,42 +39,87 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.getLicenseStatus = exports.licenseLoginGuard = exports.licenseWriteGuard = exports.licenseGuard = exports.getLicenseLimits = exports.isLicenseFeatureEnabled = void 0;
|
|
40
40
|
var cache = null;
|
|
41
41
|
var CACHE_TTL = 1000 * 60 * 60; // 1 hour
|
|
42
|
+
/**
|
|
43
|
+
* Where to fetch license data from. Two modes:
|
|
44
|
+
*
|
|
45
|
+
* - LICENSE_PROXY_URL set → in-cluster proxy (recommended).
|
|
46
|
+
* All MS except ms-entity hit `${LICENSE_PROXY_URL}` which resolves to
|
|
47
|
+
* `http://msentities-clusterip-srv:8092/api_entities/internal/license-status`.
|
|
48
|
+
* Only ms-entity itself actually talks to licensing.fluyapp.io. Dramatic
|
|
49
|
+
* reduction in outbound traffic (N × M → 1 × M).
|
|
50
|
+
*
|
|
51
|
+
* - LICENSE_URL set → direct fetch from the licensing server.
|
|
52
|
+
* Used by ms-entity (which IS the aggregator) and as fallback for any MS
|
|
53
|
+
* where the proxy is unreachable.
|
|
54
|
+
*
|
|
55
|
+
* If both are set, the proxy is tried first; on failure we fall back to the
|
|
56
|
+
* direct URL so a network issue between MS doesn't break licensing for
|
|
57
|
+
* everyone.
|
|
58
|
+
*/
|
|
42
59
|
function fetchLicense() {
|
|
43
60
|
return __awaiter(this, void 0, void 0, function () {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
function tryFetch(url, isProxy) {
|
|
62
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
63
|
+
var controller_1, timeout_1, headers, res, data, _a;
|
|
64
|
+
return __generator(this, function (_b) {
|
|
65
|
+
switch (_b.label) {
|
|
66
|
+
case 0:
|
|
67
|
+
_b.trys.push([0, 3, , 4]);
|
|
68
|
+
controller_1 = new AbortController();
|
|
69
|
+
timeout_1 = setTimeout(function () { return controller_1.abort(); }, 5000);
|
|
70
|
+
headers = {};
|
|
71
|
+
if (isProxy && internalSecret)
|
|
72
|
+
headers["x-internal-secret"] = internalSecret;
|
|
73
|
+
return [4 /*yield*/, fetch(url, { signal: controller_1.signal, headers: headers })
|
|
74
|
+
.finally(function () { return clearTimeout(timeout_1); })];
|
|
75
|
+
case 1:
|
|
76
|
+
res = _b.sent();
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
return [2 /*return*/, null];
|
|
79
|
+
return [4 /*yield*/, res.json()];
|
|
80
|
+
case 2:
|
|
81
|
+
data = _b.sent();
|
|
82
|
+
return [2 /*return*/, {
|
|
83
|
+
valid: data.valid,
|
|
84
|
+
readOnly: data.readOnly || false,
|
|
85
|
+
blocked: data.blocked || false,
|
|
86
|
+
features: data.features || {},
|
|
87
|
+
limits: data.limits || {},
|
|
88
|
+
tier: data.tier || "BASIC",
|
|
89
|
+
fetchedAt: Date.now(),
|
|
90
|
+
}];
|
|
91
|
+
case 3:
|
|
92
|
+
_a = _b.sent();
|
|
93
|
+
return [2 /*return*/, null];
|
|
94
|
+
case 4: return [2 /*return*/];
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
var proxyUrl, licenseUrl, installationUuid, internalSecret, cached, cached;
|
|
100
|
+
return __generator(this, function (_a) {
|
|
101
|
+
switch (_a.label) {
|
|
47
102
|
case 0:
|
|
103
|
+
proxyUrl = process.env.LICENSE_PROXY_URL || "";
|
|
48
104
|
licenseUrl = process.env.LICENSE_URL || "";
|
|
49
105
|
installationUuid = process.env.INSTALLATION_UUID || process.env.ENTITY_UUID || "";
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
106
|
+
internalSecret = process.env.INTERNAL_SECRET || "";
|
|
107
|
+
if (!proxyUrl) return [3 /*break*/, 2];
|
|
108
|
+
return [4 /*yield*/, tryFetch(proxyUrl, true)];
|
|
53
109
|
case 1:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
110
|
+
cached = _a.sent();
|
|
111
|
+
if (cached)
|
|
112
|
+
return [2 /*return*/, cached];
|
|
113
|
+
_a.label = 2;
|
|
58
114
|
case 2:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return [2 /*return*/, null];
|
|
62
|
-
return [4 /*yield*/, res.json()];
|
|
115
|
+
if (!(licenseUrl && installationUuid)) return [3 /*break*/, 4];
|
|
116
|
+
return [4 /*yield*/, tryFetch(licenseUrl + "/api/license/validate/" + installationUuid, false)];
|
|
63
117
|
case 3:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
features: data.features || {},
|
|
70
|
-
limits: data.limits || {},
|
|
71
|
-
tier: data.tier || "BASIC",
|
|
72
|
-
fetchedAt: Date.now(),
|
|
73
|
-
}];
|
|
74
|
-
case 4:
|
|
75
|
-
_a = _b.sent();
|
|
76
|
-
return [2 /*return*/, null];
|
|
77
|
-
case 5: return [2 /*return*/];
|
|
118
|
+
cached = _a.sent();
|
|
119
|
+
if (cached)
|
|
120
|
+
return [2 /*return*/, cached];
|
|
121
|
+
_a.label = 4;
|
|
122
|
+
case 4: return [2 /*return*/, null];
|
|
78
123
|
}
|
|
79
124
|
});
|
|
80
125
|
});
|