@fluyappgocore/commons-backend 1.0.210 → 1.0.212

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.
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Ensures a PostgreSQL database exists before connecting.
2
+ * Ensures a database exists before connecting.
3
+ * Supports PostgreSQL, MySQL, and MSSQL.
3
4
  * Uses dynamic require of 'sequelize' which is available in each MS.
4
5
  */
5
6
  export declare function ensureDatabase(opts: {
@@ -8,4 +9,6 @@ export declare function ensureDatabase(opts: {
8
9
  dbPassword: string;
9
10
  dbHost: string;
10
11
  ssl?: boolean;
12
+ dialect?: string;
13
+ port?: number;
11
14
  }): Promise<void>;
@@ -38,58 +38,82 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ensureDatabase = void 0;
40
40
  /**
41
- * Ensures a PostgreSQL database exists before connecting.
41
+ * Ensures a database exists before connecting.
42
+ * Supports PostgreSQL, MySQL, and MSSQL.
42
43
  * Uses dynamic require of 'sequelize' which is available in each MS.
43
44
  */
44
45
  function ensureDatabase(opts) {
45
46
  var _a;
46
47
  return __awaiter(this, void 0, void 0, function () {
47
- var dbName, dbUser, dbPassword, dbHost, ssl, Sequelize, adminConnection, results, error_1, _b;
48
+ var dbName, dbUser, dbPassword, dbHost, ssl, dialect, port, Sequelize, systemDb, adminConnection, results, results, error_1, code, _b;
48
49
  return __generator(this, function (_c) {
49
50
  switch (_c.label) {
50
51
  case 0:
51
52
  dbName = opts.dbName, dbUser = opts.dbUser, dbPassword = opts.dbPassword, dbHost = opts.dbHost, ssl = opts.ssl;
53
+ dialect = opts.dialect || process.env.DB_DIALECT || "postgres";
54
+ port = opts.port || parseInt(process.env.DB_PORT || "5432", 10);
52
55
  _c.label = 1;
53
56
  case 1:
54
- _c.trys.push([1, 10, , 11]);
57
+ _c.trys.push([1, 16, , 17]);
55
58
  Sequelize = require("sequelize").Sequelize;
56
- adminConnection = new Sequelize("postgres", dbUser, dbPassword, {
59
+ systemDb = dialect === "mssql" ? "master" : dialect === "mysql" ? undefined : "postgres";
60
+ adminConnection = new Sequelize(systemDb || "", dbUser, dbPassword, {
57
61
  host: dbHost,
58
- dialect: "postgres",
62
+ port: port,
63
+ dialect: dialect,
59
64
  logging: false,
60
- dialectOptions: ssl
61
- ? { ssl: { require: true, rejectUnauthorized: false } }
62
- : {},
65
+ dialectOptions: dialect === "mssql"
66
+ ? { options: { encrypt: !!ssl, trustServerCertificate: true } }
67
+ : ssl
68
+ ? { ssl: { require: true, rejectUnauthorized: false } }
69
+ : {},
63
70
  });
64
71
  _c.label = 2;
65
72
  case 2:
66
- _c.trys.push([2, 6, 7, 9]);
67
- return [4 /*yield*/, adminConnection.query("SELECT 1 FROM pg_database WHERE datname = '" + dbName + "'")];
73
+ _c.trys.push([2, 12, 13, 15]);
74
+ if (!(dialect === "mssql")) return [3 /*break*/, 6];
75
+ return [4 /*yield*/, adminConnection.query("SELECT name FROM sys.databases WHERE name = N'" + dbName + "'")];
68
76
  case 3:
69
77
  results = (_c.sent())[0];
70
78
  if (!(results.length === 0)) return [3 /*break*/, 5];
71
- return [4 /*yield*/, adminConnection.query("CREATE DATABASE \"" + dbName + "\"")];
79
+ return [4 /*yield*/, adminConnection.query("CREATE DATABASE [" + dbName + "]")];
72
80
  case 4:
73
81
  _c.sent();
74
- console.log("[DB] Database \"" + dbName + "\" created successfully");
82
+ console.log("[DB] Database \"" + dbName + "\" created (mssql)");
75
83
  _c.label = 5;
76
- case 5: return [3 /*break*/, 9];
84
+ case 5: return [3 /*break*/, 11];
77
85
  case 6:
86
+ if (!(dialect === "mysql" || dialect === "mariadb")) return [3 /*break*/, 8];
87
+ return [4 /*yield*/, adminConnection.query("CREATE DATABASE IF NOT EXISTS `" + dbName + "`")];
88
+ case 7:
89
+ _c.sent();
90
+ return [3 /*break*/, 11];
91
+ case 8: return [4 /*yield*/, adminConnection.query("SELECT 1 FROM pg_database WHERE datname = '" + dbName + "'")];
92
+ case 9:
93
+ results = (_c.sent())[0];
94
+ if (!(results.length === 0)) return [3 /*break*/, 11];
95
+ return [4 /*yield*/, adminConnection.query("CREATE DATABASE \"" + dbName + "\"")];
96
+ case 10:
97
+ _c.sent();
98
+ console.log("[DB] Database \"" + dbName + "\" created (postgres)");
99
+ _c.label = 11;
100
+ case 11: return [3 /*break*/, 15];
101
+ case 12:
78
102
  error_1 = _c.sent();
79
- // 42P04 = database already exists (race condition safe)
80
- if (((_a = error_1 === null || error_1 === void 0 ? void 0 : error_1.original) === null || _a === void 0 ? void 0 : _a.code) !== "42P04") {
103
+ code = (_a = error_1 === null || error_1 === void 0 ? void 0 : error_1.original) === null || _a === void 0 ? void 0 : _a.code;
104
+ if (code !== "42P04" && code !== 1007 && code !== "S0001") {
81
105
  console.error("[DB] Error ensuring database \"" + dbName + "\":", error_1.message);
82
106
  }
83
- return [3 /*break*/, 9];
84
- case 7: return [4 /*yield*/, adminConnection.close()];
85
- case 8:
107
+ return [3 /*break*/, 15];
108
+ case 13: return [4 /*yield*/, adminConnection.close()];
109
+ case 14:
86
110
  _c.sent();
87
111
  return [7 /*endfinally*/];
88
- case 9: return [3 /*break*/, 11];
89
- case 10:
112
+ case 15: return [3 /*break*/, 17];
113
+ case 16:
90
114
  _b = _c.sent();
91
- return [3 /*break*/, 11];
92
- case 11: return [2 /*return*/];
115
+ return [3 /*break*/, 17];
116
+ case 17: return [2 /*return*/];
93
117
  }
94
118
  });
95
119
  });
@@ -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;
@@ -2,3 +2,4 @@ export * from './auth.middleware';
2
2
  export * from './error.middleware';
3
3
  export * from './validation.middleware';
4
4
  export * from './licenseGuard.middleware';
5
+ export * from './guestAuth.middleware';
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluyappgocore/commons-backend",
3
- "version": "1.0.210",
3
+ "version": "1.0.212",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",