@mxpicture/gcp-functions 0.1.0
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 +38 -0
- package/dist/cjs/FunctionHandlers.js +18 -0
- package/dist/cjs/api/IApi.js +52 -0
- package/dist/cjs/common/firebase.js +174 -0
- package/dist/cjs/common/http.js +147 -0
- package/dist/cjs/common/types.common.js +30 -0
- package/dist/cjs/handler/HttpHandler.js +41 -0
- package/dist/cjs/handler/IHttpHandlerItem.js +169 -0
- package/dist/cjs/index.js +42 -0
- package/dist/cjs/lib.js +25 -0
- package/dist/cjs/package.js +21 -0
- package/dist/cjs/store/IStore.js +107 -0
- package/dist/cjs/store/types.store.js +15 -0
- package/dist/esm/FunctionHandlers.js +18 -0
- package/dist/esm/api/IApi.js +52 -0
- package/dist/esm/common/firebase.js +174 -0
- package/dist/esm/common/http.js +147 -0
- package/dist/esm/common/types.common.js +30 -0
- package/dist/esm/handler/HttpHandler.js +41 -0
- package/dist/esm/handler/IHttpHandlerItem.js +169 -0
- package/dist/esm/index.js +42 -0
- package/dist/esm/lib.js +25 -0
- package/dist/esm/package.js +21 -0
- package/dist/esm/store/IStore.js +107 -0
- package/dist/esm/store/types.store.js +15 -0
- package/dist/types/FunctionHandlers.d.ts +12 -0
- package/dist/types/api/IApi.d.ts +27 -0
- package/dist/types/common/firebase.d.ts +11 -0
- package/dist/types/common/http.d.ts +20 -0
- package/dist/types/common/types.common.d.ts +31 -0
- package/dist/types/handler/HttpHandler.d.ts +15 -0
- package/dist/types/handler/IHttpHandlerItem.d.ts +23 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/lib.d.ts +11 -0
- package/dist/types/package.d.ts +1 -0
- package/dist/types/store/IStore.d.ts +25 -0
- package/dist/types/store/types.store.d.ts +23 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# gcp-functions-lib
|
|
2
|
+
|
|
3
|
+
Example Node library using TypeScript and Zod. Produces:
|
|
4
|
+
|
|
5
|
+
- CommonJS build at `dist/cjs`
|
|
6
|
+
- ESM build at `dist/esm`
|
|
7
|
+
- Type declarations at `dist/types`
|
|
8
|
+
|
|
9
|
+
Usage (after npm publish or local link):
|
|
10
|
+
|
|
11
|
+
ESM:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { validateUser } from "gcp-functions-lib";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
CommonJS:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const { validateUser } = require("gcp-functions-lib");
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Build:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
npm run build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Setup husky
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install --save-dev @commitlint/config-conventional @commitlint/cli husky commitizen cz-conventional-changelog husky
|
|
34
|
+
npx husky-init
|
|
35
|
+
npm i
|
|
36
|
+
npm run prepare
|
|
37
|
+
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
|
|
38
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FunctionHandlers = void 0;
|
|
4
|
+
class FunctionHandlers {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.items = {};
|
|
7
|
+
}
|
|
8
|
+
add(handler) {
|
|
9
|
+
this.items[handler.name] = handler;
|
|
10
|
+
}
|
|
11
|
+
functions() {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const [key, item] of Object.entries(this.items))
|
|
14
|
+
result[key] = item.setup();
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.FunctionHandlers = FunctionHandlers;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IApi = void 0;
|
|
4
|
+
const zod_meta_1 = require("@mxpicture/zod-meta");
|
|
5
|
+
class IApi {
|
|
6
|
+
constructor(store, shape) {
|
|
7
|
+
this.store = store;
|
|
8
|
+
this.shape = shape;
|
|
9
|
+
}
|
|
10
|
+
metaItems() {
|
|
11
|
+
return new zod_meta_1.Meta(this.shape).items();
|
|
12
|
+
}
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
14
|
+
async create(doc, params) {
|
|
15
|
+
const storeDoc = await this.store.create(doc);
|
|
16
|
+
return this.fromStore(storeDoc);
|
|
17
|
+
}
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
19
|
+
async get(id, params) {
|
|
20
|
+
const storeDoc = await this.store.get(id);
|
|
21
|
+
return this.fromStore(storeDoc);
|
|
22
|
+
}
|
|
23
|
+
async query(params) {
|
|
24
|
+
const storeDocs = await this.store.query(params);
|
|
25
|
+
return await Promise.all(storeDocs.map((s) => this.fromStore(s)));
|
|
26
|
+
}
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
28
|
+
async count(params) {
|
|
29
|
+
return this.store.count();
|
|
30
|
+
}
|
|
31
|
+
async update(id, doc,
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
33
|
+
params) {
|
|
34
|
+
let storeDoc = await this.toStore(doc);
|
|
35
|
+
storeDoc = await this.store.update(id, storeDoc);
|
|
36
|
+
return this.fromStore(storeDoc);
|
|
37
|
+
}
|
|
38
|
+
async updatePartial(id, doc) {
|
|
39
|
+
const storeDoc = await this.toStorePartial(doc);
|
|
40
|
+
await this.store.update(id, storeDoc);
|
|
41
|
+
return this.get(id);
|
|
42
|
+
}
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
44
|
+
async delete(id, params) {
|
|
45
|
+
await this.store.delete(id);
|
|
46
|
+
}
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
48
|
+
async exists(id, params) {
|
|
49
|
+
return this.store.exists(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.IApi = IApi;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.round = exports.verifyConnectionToken = exports.sessionLogout = exports.sessionLogin = exports.verifyAuthAll = exports.verifySessionCookie = exports.verifyToken = exports.extractToken = exports.db = exports.app = void 0;
|
|
4
|
+
const auth_1 = require("firebase-admin/auth");
|
|
5
|
+
const firestore_1 = require("firebase-admin/firestore");
|
|
6
|
+
const app_1 = require("firebase-admin/app");
|
|
7
|
+
exports.app = (0, app_1.initializeApp)();
|
|
8
|
+
exports.db = (0, firestore_1.getFirestore)();
|
|
9
|
+
const extractToken = (request) => {
|
|
10
|
+
const auth = request.headers["authorization"];
|
|
11
|
+
if (!auth)
|
|
12
|
+
return;
|
|
13
|
+
// Read the ID Token from the Authorization header.
|
|
14
|
+
if (auth && auth.startsWith("Bearer "))
|
|
15
|
+
return auth.split("Bearer ")[1];
|
|
16
|
+
// Read the ID Token from cookie.
|
|
17
|
+
if (request.cookies)
|
|
18
|
+
return request.cookies.__session;
|
|
19
|
+
return;
|
|
20
|
+
};
|
|
21
|
+
exports.extractToken = extractToken;
|
|
22
|
+
// Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
|
|
23
|
+
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
|
|
24
|
+
// `Authorization: Bearer <Firebase ID Token>`.
|
|
25
|
+
// when decoded successfully, the ID Token content will be added as `request.user`.
|
|
26
|
+
const verifyToken = (request, response, next) => {
|
|
27
|
+
const token = (0, exports.extractToken)(request);
|
|
28
|
+
if (!token) {
|
|
29
|
+
response
|
|
30
|
+
.status(401)
|
|
31
|
+
.json({ status: "unauthorized", message: "Unauthorized" });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
(0, auth_1.getAuth)()
|
|
35
|
+
.verifyIdToken(token)
|
|
36
|
+
.then((decodedIdToken) => {
|
|
37
|
+
// Only process if the user just signed in in the last 5 minutes.
|
|
38
|
+
if (Date.now() / 1000 - decodedIdToken.auth_time >= 5 * 60)
|
|
39
|
+
return response.status(401).json({
|
|
40
|
+
status: "unauthorized",
|
|
41
|
+
message: "Recent sign in required!",
|
|
42
|
+
});
|
|
43
|
+
request.user = decodedIdToken;
|
|
44
|
+
request.headers["x-id-token"] = token;
|
|
45
|
+
return next();
|
|
46
|
+
}, (error) => {
|
|
47
|
+
response.status(401).json({
|
|
48
|
+
status: "unauthorized",
|
|
49
|
+
message: JSON.stringify(error),
|
|
50
|
+
});
|
|
51
|
+
})
|
|
52
|
+
.catch((error) => {
|
|
53
|
+
response.status(401).json({
|
|
54
|
+
status: "unauthorized",
|
|
55
|
+
message: `Error while verifying Firebase ID token: ${error}`,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
exports.verifyToken = verifyToken;
|
|
60
|
+
const verifySessionCookie = (request, response, next) => {
|
|
61
|
+
const sessionCookie =
|
|
62
|
+
// request.cookies.session ||
|
|
63
|
+
// request.cookies.zsession ||
|
|
64
|
+
request.cookies.__session || request.headers.zsession || "";
|
|
65
|
+
// Verify the session cookie. In this case an additional check is added to detect
|
|
66
|
+
// if the user's Firebase session was revoked, user deleted/disabled, etc.
|
|
67
|
+
(0, auth_1.getAuth)()
|
|
68
|
+
.verifySessionCookie(sessionCookie, true /** checkRevoked */)
|
|
69
|
+
.then(() => {
|
|
70
|
+
next();
|
|
71
|
+
}, (error) => {
|
|
72
|
+
response.status(401).json({
|
|
73
|
+
status: "unauthorized",
|
|
74
|
+
message: JSON.stringify(error),
|
|
75
|
+
});
|
|
76
|
+
})
|
|
77
|
+
.catch((error) => {
|
|
78
|
+
response.status(401).json({
|
|
79
|
+
status: "unauthorized",
|
|
80
|
+
message: JSON.stringify(error),
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
exports.verifySessionCookie = verifySessionCookie;
|
|
85
|
+
const verifyAuthAll = (token) => {
|
|
86
|
+
return (request, response, next) => {
|
|
87
|
+
const t = token();
|
|
88
|
+
// connection token
|
|
89
|
+
const id = request.headers["x-connection-token"];
|
|
90
|
+
if (typeof id === "string") {
|
|
91
|
+
if (id === t)
|
|
92
|
+
return next();
|
|
93
|
+
return response.status(401).json({
|
|
94
|
+
status: "unauthorized",
|
|
95
|
+
message: "",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// message connection token
|
|
99
|
+
if ((0, exports.extractToken)(request))
|
|
100
|
+
return (0, exports.verifyToken)(request, response, next);
|
|
101
|
+
// session cookie
|
|
102
|
+
return (0, exports.verifySessionCookie)(request, response, next);
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
exports.verifyAuthAll = verifyAuthAll;
|
|
106
|
+
const sessionLogin = (request, response) => {
|
|
107
|
+
// Get the ID token passed and the CSRF token.
|
|
108
|
+
const idToken = `${request.headers["x-id-token"]}`;
|
|
109
|
+
// Set session expiration to 5 days.
|
|
110
|
+
const expiresIn = 60 * 60 * 24 * 5 * 1000;
|
|
111
|
+
// Create the session cookie. This will also verify the ID token in the process.
|
|
112
|
+
// The session cookie will have the same claims as the ID token.
|
|
113
|
+
// To only allow session cookie setting on recent sign-in, auth_time in ID token
|
|
114
|
+
// can be checked to ensure user was recently signed in before creating a session cookie.
|
|
115
|
+
(0, auth_1.getAuth)()
|
|
116
|
+
.createSessionCookie(idToken, { expiresIn })
|
|
117
|
+
.then((sessionCookie) => {
|
|
118
|
+
// Set cookie policy for session cookie.
|
|
119
|
+
// cloud functions will only allow __session (issue only deployed)
|
|
120
|
+
response.cookie("__session", sessionCookie, {
|
|
121
|
+
maxAge: expiresIn,
|
|
122
|
+
httpOnly: true,
|
|
123
|
+
secure: true,
|
|
124
|
+
});
|
|
125
|
+
// const now = new Date();
|
|
126
|
+
// const zsession =
|
|
127
|
+
// "zsession=" +
|
|
128
|
+
// sessionCookie +
|
|
129
|
+
// "; expires=" +
|
|
130
|
+
// now.toUTCString() +
|
|
131
|
+
// "; path=/";
|
|
132
|
+
response.end(JSON.stringify({
|
|
133
|
+
status: "success",
|
|
134
|
+
// zsession: zsession,
|
|
135
|
+
}));
|
|
136
|
+
}, (error) => {
|
|
137
|
+
response.status(401).json({
|
|
138
|
+
status: "unauthorized",
|
|
139
|
+
message: JSON.stringify(error),
|
|
140
|
+
});
|
|
141
|
+
})
|
|
142
|
+
.catch((error) => {
|
|
143
|
+
response.status(401).json({
|
|
144
|
+
status: "unauthorized",
|
|
145
|
+
message: JSON.stringify(error),
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
exports.sessionLogin = sessionLogin;
|
|
150
|
+
const sessionLogout = (request, response) => {
|
|
151
|
+
response.clearCookie("session");
|
|
152
|
+
response.clearCookie("zsession");
|
|
153
|
+
response.clearCookie("__session");
|
|
154
|
+
};
|
|
155
|
+
exports.sessionLogout = sessionLogout;
|
|
156
|
+
const verifyConnectionToken = (token) => {
|
|
157
|
+
return async (request, response, next) => {
|
|
158
|
+
const id = request.headers["x-connection-token"];
|
|
159
|
+
if (id === token()) {
|
|
160
|
+
next();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
response.status(401).json({
|
|
164
|
+
status: "unauthorized",
|
|
165
|
+
message: "",
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
exports.verifyConnectionToken = verifyConnectionToken;
|
|
170
|
+
const round = (value, digits = 3) => {
|
|
171
|
+
const factor = Math.pow(10, digits);
|
|
172
|
+
return Math.round(value * factor) / factor;
|
|
173
|
+
};
|
|
174
|
+
exports.round = round;
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.fixRequestBody = exports.hasBody = exports.contentstream = void 0;
|
|
40
|
+
const querystring = __importStar(require("querystring"));
|
|
41
|
+
const zlib_1 = require("zlib");
|
|
42
|
+
const zlib = __importStar(require("zlib"));
|
|
43
|
+
const http_errors_1 = __importDefault(require("http-errors"));
|
|
44
|
+
// export const handleErrorMessage = (
|
|
45
|
+
// response: express.Response,
|
|
46
|
+
// error: unknown,
|
|
47
|
+
// code: number = StatusCodes.BAD_REQUEST,
|
|
48
|
+
// ) => {
|
|
49
|
+
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
// const ae = error as any;
|
|
51
|
+
// const result = { message: "" };
|
|
52
|
+
// if (error instanceof Error) result.message = error.message;
|
|
53
|
+
// else if (typeof error === "string") result.message = error;
|
|
54
|
+
// else if (ae.message) result.message = ae.message;
|
|
55
|
+
// else if (ae.payload) {
|
|
56
|
+
// if (ae.payload.message && typeof ae.payload.message === "string")
|
|
57
|
+
// result.message = ae.payload.message;
|
|
58
|
+
// else if (typeof ae.payload === "string") result.message = ae.payload;
|
|
59
|
+
// } else result.message = "unknown";
|
|
60
|
+
// return response.status(code).json(result);
|
|
61
|
+
// };
|
|
62
|
+
const hasBrotliSupport = "createBrotliDecompress" in zlib;
|
|
63
|
+
/**
|
|
64
|
+
* Get the content stream of the request.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} req
|
|
67
|
+
* @param {function} debug
|
|
68
|
+
* @param {boolean} [inflate=true]
|
|
69
|
+
* @return {object}
|
|
70
|
+
* @api private
|
|
71
|
+
*/
|
|
72
|
+
const contentstream = (req, inflate, debug = console) => {
|
|
73
|
+
const encoding = (req.headers["content-encoding"] || "identity").toLowerCase();
|
|
74
|
+
// const length = req.headers["content-length"];
|
|
75
|
+
let stream;
|
|
76
|
+
debug?.log('content-encoding "%s"', encoding);
|
|
77
|
+
if (inflate === false && encoding !== "identity") {
|
|
78
|
+
throw (0, http_errors_1.default)(415, "content encoding unsupported", {
|
|
79
|
+
encoding: encoding,
|
|
80
|
+
type: "encoding.unsupported",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
switch (encoding) {
|
|
84
|
+
case "deflate":
|
|
85
|
+
stream = (0, zlib_1.createInflate)();
|
|
86
|
+
debug?.log("inflate body");
|
|
87
|
+
req.pipe(stream);
|
|
88
|
+
break;
|
|
89
|
+
case "gzip":
|
|
90
|
+
stream = (0, zlib_1.createGunzip)();
|
|
91
|
+
debug?.log("gunzip body");
|
|
92
|
+
req.pipe(stream);
|
|
93
|
+
break;
|
|
94
|
+
case "identity":
|
|
95
|
+
stream = req;
|
|
96
|
+
// stream.length = length;
|
|
97
|
+
break;
|
|
98
|
+
case "br":
|
|
99
|
+
if (hasBrotliSupport) {
|
|
100
|
+
stream = (0, zlib_1.createBrotliDecompress)();
|
|
101
|
+
debug?.log("brotli decompress body");
|
|
102
|
+
req.pipe(stream);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (stream === undefined) {
|
|
107
|
+
throw (0, http_errors_1.default)(415, 'unsupported content encoding "' + encoding + '"', {
|
|
108
|
+
encoding: encoding,
|
|
109
|
+
type: "encoding.unsupported",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return stream;
|
|
113
|
+
};
|
|
114
|
+
exports.contentstream = contentstream;
|
|
115
|
+
const hasBody = (request) => {
|
|
116
|
+
const contentType = request.headers["content-type"];
|
|
117
|
+
return contentType
|
|
118
|
+
? contentType.includes("application/json") ||
|
|
119
|
+
contentType.includes("+json") ||
|
|
120
|
+
contentType.includes("application/x-www-form-urlencoded")
|
|
121
|
+
: false;
|
|
122
|
+
};
|
|
123
|
+
exports.hasBody = hasBody;
|
|
124
|
+
/**
|
|
125
|
+
* Fix proxied body if bodyParser is involved.
|
|
126
|
+
*/
|
|
127
|
+
const fixRequestBody = (proxyReq, requestBody) => {
|
|
128
|
+
if (!requestBody) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const contentType = proxyReq.getHeader("Content-Type");
|
|
132
|
+
const writeBody = (bodyData) => {
|
|
133
|
+
// deepcode ignore ContentLengthInCode: bodyParser fix
|
|
134
|
+
proxyReq.setHeader("Content-Length", `'${Buffer.byteLength(bodyData)}'`);
|
|
135
|
+
proxyReq.write(bodyData);
|
|
136
|
+
};
|
|
137
|
+
if (contentType &&
|
|
138
|
+
(contentType.includes("application/json") || contentType.includes("+json"))) {
|
|
139
|
+
writeBody(JSON.stringify(requestBody));
|
|
140
|
+
}
|
|
141
|
+
if (contentType &&
|
|
142
|
+
contentType.includes("application/x-www-form-urlencoded") &&
|
|
143
|
+
!Array.isArray(requestBody)) {
|
|
144
|
+
writeBody(querystring.stringify(requestBody));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
exports.fixRequestBody = fixRequestBody;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.castHttpError = exports.PrimitiveType = exports.HttpMethod = exports.HttpProtocol = void 0;
|
|
4
|
+
var HttpProtocol;
|
|
5
|
+
(function (HttpProtocol) {
|
|
6
|
+
HttpProtocol["http"] = "http";
|
|
7
|
+
HttpProtocol["https"] = "https";
|
|
8
|
+
})(HttpProtocol || (exports.HttpProtocol = HttpProtocol = {}));
|
|
9
|
+
var HttpMethod;
|
|
10
|
+
(function (HttpMethod) {
|
|
11
|
+
HttpMethod["POST"] = "POST";
|
|
12
|
+
HttpMethod["PATCH"] = "PATCH";
|
|
13
|
+
HttpMethod["PUT"] = "PUT";
|
|
14
|
+
HttpMethod["GET"] = "GET";
|
|
15
|
+
HttpMethod["DELETE"] = "DELETE";
|
|
16
|
+
})(HttpMethod || (exports.HttpMethod = HttpMethod = {}));
|
|
17
|
+
var PrimitiveType;
|
|
18
|
+
(function (PrimitiveType) {
|
|
19
|
+
PrimitiveType["string"] = "string";
|
|
20
|
+
PrimitiveType["number"] = "number";
|
|
21
|
+
PrimitiveType["boolean"] = "boolean";
|
|
22
|
+
})(PrimitiveType || (exports.PrimitiveType = PrimitiveType = {}));
|
|
23
|
+
const castHttpError = (error) => {
|
|
24
|
+
if (!error.message || typeof error.message !== "string")
|
|
25
|
+
return;
|
|
26
|
+
if (error.context && typeof error.message !== "string")
|
|
27
|
+
return;
|
|
28
|
+
return { message: error.message, context: error.context };
|
|
29
|
+
};
|
|
30
|
+
exports.castHttpError = castHttpError;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HttpHandler = void 0;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
|
9
|
+
const cors_1 = __importDefault(require("cors"));
|
|
10
|
+
const https_1 = require("firebase-functions/v2/https");
|
|
11
|
+
// export interface HandlerEnv {
|
|
12
|
+
// connectionToken: () => string;
|
|
13
|
+
// }
|
|
14
|
+
class HttpHandler {
|
|
15
|
+
constructor(name) {
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.corsAllowList = [];
|
|
18
|
+
this.items = [];
|
|
19
|
+
this.exp = (0, express_1.default)();
|
|
20
|
+
}
|
|
21
|
+
add(...items) {
|
|
22
|
+
this.items.push(...items);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
baseSetup() {
|
|
26
|
+
this.exp.use((0, cors_1.default)({ credentials: true, origin: true }));
|
|
27
|
+
this.exp.use((0, cookie_parser_1.default)());
|
|
28
|
+
this.exp.disable("x-powered-by");
|
|
29
|
+
// this.exp.use(verifySessionCookie); // todo verify token (maybe connection token)
|
|
30
|
+
}
|
|
31
|
+
setup() {
|
|
32
|
+
this.baseSetup();
|
|
33
|
+
for (const item of this.items)
|
|
34
|
+
item.setup(this.exp);
|
|
35
|
+
return this.createFunction();
|
|
36
|
+
}
|
|
37
|
+
createFunction() {
|
|
38
|
+
return (0, https_1.onRequest)({ cors: [/firebase\.com$/, /web\.app$/] }, this.exp);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.HttpHandler = HttpHandler;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.IHttpHandlerItem = void 0;
|
|
7
|
+
const raw_body_1 = __importDefault(require("raw-body"));
|
|
8
|
+
const http_js_1 = require("../common/http.js");
|
|
9
|
+
const https_1 = require("firebase-functions/v2/https");
|
|
10
|
+
const zod_1 = require("zod");
|
|
11
|
+
const types_common_js_1 = require("../common/types.common.js");
|
|
12
|
+
const http_status_codes_1 = require("@mxpicture/http-status-codes");
|
|
13
|
+
// export interface HandlerEnv {
|
|
14
|
+
// connectionToken: () => string;
|
|
15
|
+
// }
|
|
16
|
+
class IHttpHandlerItem {
|
|
17
|
+
constructor(name, api) {
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.api = api;
|
|
20
|
+
this.corsAllowList = [];
|
|
21
|
+
}
|
|
22
|
+
setup(exp) {
|
|
23
|
+
exp.get(`/api/${this.name}`, async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const items = await this.api.query();
|
|
26
|
+
return this.httpSuccess(res, items);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return this.httpErrorUnnown(res, error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
exp.get(`/api/${this.name}_meta`, async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const items = this.api.metaItems();
|
|
35
|
+
return this.httpSuccess(res, items);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return this.httpErrorUnnown(res, error);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
exp.get(`/api/${this.name}/:id`, async (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const item = await this.api.get(req.params.id);
|
|
44
|
+
return this.httpSuccess(res, item);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return this.httpErrorUnnown(res, error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
exp.post(`/api/${this.name}`, async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const item = await this.extractBodyItem(req);
|
|
53
|
+
const valResult = await this.api.validate(item);
|
|
54
|
+
if (!valResult.success)
|
|
55
|
+
throw (0, zod_1.treeifyError)(valResult.error);
|
|
56
|
+
const validated = await this.api.create(valResult.data);
|
|
57
|
+
return this.httpSuccess(res, validated);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return this.httpErrorUnnown(res, error);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
exp.patch(`/api/${this.name}/:id`, async (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const item = await this.extractBodyItem(req);
|
|
66
|
+
const valResult = await this.api.validatePartial(item);
|
|
67
|
+
if (!valResult.success)
|
|
68
|
+
throw (0, zod_1.treeifyError)(valResult.error);
|
|
69
|
+
const validated = await this.api.updatePartial(req.params.id, valResult.data);
|
|
70
|
+
return this.httpSuccess(res, validated);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return this.httpErrorUnnown(res, error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
httpSuccess(res, payload, statusCode = http_status_codes_1.StatusCodes.OK) {
|
|
78
|
+
const result = { statusCode, payload };
|
|
79
|
+
return res.status(statusCode).json(result);
|
|
80
|
+
}
|
|
81
|
+
httpError(res, errors, statusCode = http_status_codes_1.StatusCodes.BAD_REQUEST) {
|
|
82
|
+
const result = { statusCode, errors };
|
|
83
|
+
return res.status(statusCode).json(result);
|
|
84
|
+
}
|
|
85
|
+
httpErrorUnnown(res, error, statusCode = http_status_codes_1.StatusCodes.BAD_REQUEST) {
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
const ae = error;
|
|
88
|
+
const messages = [];
|
|
89
|
+
if (error instanceof Error) {
|
|
90
|
+
messages.push({ message: error.message });
|
|
91
|
+
}
|
|
92
|
+
else if (typeof error === "string") {
|
|
93
|
+
messages.push({ message: error });
|
|
94
|
+
}
|
|
95
|
+
else if (ae.message) {
|
|
96
|
+
const err = (0, types_common_js_1.castHttpError)(ae.message);
|
|
97
|
+
if (err)
|
|
98
|
+
messages.push(err);
|
|
99
|
+
}
|
|
100
|
+
else if (ae.errors) {
|
|
101
|
+
messages.push(...this.extractZodError(ae));
|
|
102
|
+
}
|
|
103
|
+
else if (ae.payload) {
|
|
104
|
+
if (ae.payload.message && typeof ae.payload.message === "string")
|
|
105
|
+
messages.push({ message: ae.payload.message });
|
|
106
|
+
else if (typeof ae.payload === "string")
|
|
107
|
+
messages.push({ message: ae.payload });
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
messages.push({ message: "unknown" });
|
|
111
|
+
}
|
|
112
|
+
return this.httpError(res, messages, statusCode);
|
|
113
|
+
}
|
|
114
|
+
extractZodError(tree, ...contexts) {
|
|
115
|
+
const errors = tree.errors?.map((e) => ({
|
|
116
|
+
message: e,
|
|
117
|
+
context: this.contextPath(...contexts),
|
|
118
|
+
})) ?? [];
|
|
119
|
+
const items = "items" in tree && tree.items
|
|
120
|
+
? tree.items
|
|
121
|
+
: "properties" in tree && tree.properties
|
|
122
|
+
? tree.properties
|
|
123
|
+
: [];
|
|
124
|
+
if (Array.isArray(items)) {
|
|
125
|
+
for (let i = 0; i < items.length; i++)
|
|
126
|
+
errors.push(...this.extractZodError(items[i], ...contexts, i));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
for (const [key, item] of Object.entries(items))
|
|
130
|
+
errors.push(...this.extractZodError(item, ...contexts, key));
|
|
131
|
+
}
|
|
132
|
+
return errors;
|
|
133
|
+
}
|
|
134
|
+
contextPath(...parts) {
|
|
135
|
+
const ps = parts
|
|
136
|
+
.filter((p) => !!p)
|
|
137
|
+
.map((p) => (typeof p === "string" ? p : `${p}`));
|
|
138
|
+
return ps.join(".");
|
|
139
|
+
}
|
|
140
|
+
createFunction(exp) {
|
|
141
|
+
return (0, https_1.onRequest)({ cors: [/firebase\.com$/, /web\.app$/] }, exp);
|
|
142
|
+
}
|
|
143
|
+
async extractBody(req) {
|
|
144
|
+
if (!req.headers["content-type"])
|
|
145
|
+
throw new Error("No content type provided");
|
|
146
|
+
const body = req.body ??
|
|
147
|
+
JSON.parse(await (0, raw_body_1.default)((0, http_js_1.contentstream)(req, false), {
|
|
148
|
+
encoding: "utf-8",
|
|
149
|
+
}));
|
|
150
|
+
if (Array.isArray(body))
|
|
151
|
+
return body;
|
|
152
|
+
if (body && Object.keys(body).length > 0)
|
|
153
|
+
return body;
|
|
154
|
+
throw new Error("Invalid / not existing body");
|
|
155
|
+
}
|
|
156
|
+
async extractBodyItem(req) {
|
|
157
|
+
const extr = await this.extractBody(req);
|
|
158
|
+
if (!extr || Array.isArray(extr))
|
|
159
|
+
throw new Error(`No item found in body`);
|
|
160
|
+
return extr;
|
|
161
|
+
}
|
|
162
|
+
async extractBodyItems(req) {
|
|
163
|
+
const extr = await this.extractBody(req);
|
|
164
|
+
if (!extr || !Array.isArray(extr))
|
|
165
|
+
throw new Error(`No items found in body`);
|
|
166
|
+
return extr;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.IHttpHandlerItem = IHttpHandlerItem;
|