@takaro/http 0.0.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.
Files changed (50) hide show
  1. package/README.md +7 -0
  2. package/dist/app.d.ts +21 -0
  3. package/dist/app.js +87 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/controllers/meta.d.ts +15 -0
  6. package/dist/controllers/meta.js +186 -0
  7. package/dist/controllers/meta.js.map +1 -0
  8. package/dist/main.d.ts +6 -0
  9. package/dist/main.js +7 -0
  10. package/dist/main.js.map +1 -0
  11. package/dist/middleware/adminAuth.d.ts +2 -0
  12. package/dist/middleware/adminAuth.js +27 -0
  13. package/dist/middleware/adminAuth.js.map +1 -0
  14. package/dist/middleware/errorHandler.d.ts +2 -0
  15. package/dist/middleware/errorHandler.js +51 -0
  16. package/dist/middleware/errorHandler.js.map +1 -0
  17. package/dist/middleware/logger.d.ts +5 -0
  18. package/dist/middleware/logger.js +51 -0
  19. package/dist/middleware/logger.js.map +1 -0
  20. package/dist/middleware/metrics.d.ts +2 -0
  21. package/dist/middleware/metrics.js +38 -0
  22. package/dist/middleware/metrics.js.map +1 -0
  23. package/dist/middleware/paginationMiddleware.d.ts +2 -0
  24. package/dist/middleware/paginationMiddleware.js +26 -0
  25. package/dist/middleware/paginationMiddleware.js.map +1 -0
  26. package/dist/middleware/rateLimit.d.ts +8 -0
  27. package/dist/middleware/rateLimit.js +65 -0
  28. package/dist/middleware/rateLimit.js.map +1 -0
  29. package/dist/util/apiResponse.d.ts +36 -0
  30. package/dist/util/apiResponse.js +100 -0
  31. package/dist/util/apiResponse.js.map +1 -0
  32. package/package.json +37 -0
  33. package/src/app.ts +104 -0
  34. package/src/controllers/__tests__/meta.integration.test.ts +29 -0
  35. package/src/controllers/defaultMetadatastorage.d.ts +5 -0
  36. package/src/controllers/meta.ts +170 -0
  37. package/src/main.ts +7 -0
  38. package/src/middleware/__tests__/adminAuth.unit.test.ts +65 -0
  39. package/src/middleware/__tests__/paginationMiddleware.unit.test.ts +48 -0
  40. package/src/middleware/__tests__/rateLimit.integration.test.ts +125 -0
  41. package/src/middleware/adminAuth.ts +33 -0
  42. package/src/middleware/errorHandler.ts +67 -0
  43. package/src/middleware/logger.ts +62 -0
  44. package/src/middleware/metrics.ts +44 -0
  45. package/src/middleware/paginationMiddleware.ts +29 -0
  46. package/src/middleware/rateLimit.ts +78 -0
  47. package/src/util/apiResponse.ts +106 -0
  48. package/tsconfig.build.json +9 -0
  49. package/tsconfig.json +8 -0
  50. package/typedoc.json +3 -0
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @takaro/http
2
+
3
+ ## TODO
4
+
5
+ - [ ] Middlewares
6
+ - [ ] Validation middleware (celebrate?)
7
+ - [ ]
package/dist/app.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import 'reflect-metadata';
3
+ import express from 'express';
4
+ import { Server } from 'http';
5
+ import { RoutingControllersOptions } from 'routing-controllers';
6
+ interface IHTTPOptions {
7
+ port?: number;
8
+ allowedOrigins?: string[];
9
+ }
10
+ export declare class HTTP {
11
+ private httpOptions;
12
+ private app;
13
+ private httpServer;
14
+ private logger;
15
+ constructor(options?: RoutingControllersOptions, httpOptions?: IHTTPOptions);
16
+ get expressInstance(): express.Application;
17
+ get server(): Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
18
+ start(): Promise<void>;
19
+ stop(): Promise<void>;
20
+ }
21
+ export {};
package/dist/app.js ADDED
@@ -0,0 +1,87 @@
1
+ import 'reflect-metadata';
2
+ import express from 'express';
3
+ import { logger, errors, Sentry } from '@takaro/util';
4
+ import { getBullBoard } from '@takaro/queues';
5
+ import { createServer } from 'http';
6
+ import { useExpressServer } from 'routing-controllers';
7
+ import { Meta } from './controllers/meta.js';
8
+ import { LoggingMiddleware } from './middleware/logger.js';
9
+ import { ErrorHandler } from './middleware/errorHandler.js';
10
+ import bodyParser from 'body-parser';
11
+ import cors from 'cors';
12
+ import cookieParser from 'cookie-parser';
13
+ import { metricsMiddleware } from './main.js';
14
+ import { paginationMiddleware } from './middleware/paginationMiddleware.js';
15
+ export class HTTP {
16
+ constructor(options = {}, httpOptions = {}) {
17
+ this.httpOptions = httpOptions;
18
+ this.logger = logger('http');
19
+ this.app = express();
20
+ this.httpServer = createServer(this.app);
21
+ this.app.use(Sentry.Handlers.requestHandler());
22
+ this.app.use(bodyParser.json({
23
+ verify: (req, res, buf) => {
24
+ req.rawBody = buf.toString();
25
+ },
26
+ }));
27
+ this.app.use(LoggingMiddleware);
28
+ this.app.use(metricsMiddleware);
29
+ this.app.use(paginationMiddleware);
30
+ this.app.set('x-powered-by', false);
31
+ this.app.use(cors({
32
+ credentials: true,
33
+ exposedHeaders: ['X-Trace-Id'],
34
+ origin: (origin, callback) => {
35
+ if (!origin)
36
+ return callback(null, true);
37
+ const allowedOrigins = this.httpOptions.allowedOrigins ?? [];
38
+ if (!origin || allowedOrigins.includes(origin)) {
39
+ callback(null, true);
40
+ }
41
+ else {
42
+ this.logger.warn(`Origin ${origin} not allowed by CORS`);
43
+ callback(new errors.BadRequestError('Not allowed by CORS'));
44
+ }
45
+ },
46
+ }));
47
+ this.app.use(cookieParser());
48
+ if (options.controllers) {
49
+ useExpressServer(this.app, {
50
+ ...options,
51
+ defaultErrorHandler: false,
52
+ validation: { whitelist: true, forbidNonWhitelisted: true },
53
+ // eslint-disable-next-line @typescript-eslint/ban-types
54
+ controllers: [Meta, ...options.controllers],
55
+ });
56
+ }
57
+ else {
58
+ useExpressServer(this.app, {
59
+ ...options,
60
+ defaultErrorHandler: false,
61
+ controllers: [Meta],
62
+ validation: { whitelist: true, forbidNonWhitelisted: true },
63
+ });
64
+ }
65
+ this.app.use('/queues', getBullBoard());
66
+ this.app.use(Sentry.Handlers.errorHandler());
67
+ this.app.use(ErrorHandler);
68
+ }
69
+ get expressInstance() {
70
+ return this.app;
71
+ }
72
+ get server() {
73
+ return this.httpServer;
74
+ }
75
+ async start() {
76
+ this.httpServer = this.httpServer.listen(this.httpOptions.port, () => {
77
+ this.logger.info(`HTTP server listening on port ${this.httpOptions.port}`);
78
+ });
79
+ }
80
+ async stop() {
81
+ if (this.httpServer) {
82
+ this.httpServer.close();
83
+ this.logger.info('HTTP server stopped');
84
+ }
85
+ }
86
+ }
87
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { OpenAPIObject } from 'openapi3-ts';
2
+ export declare class HealthOutputDTO {
3
+ healthy: boolean;
4
+ }
5
+ export declare class Meta {
6
+ getHealth(): Promise<{
7
+ healthy: boolean;
8
+ }>;
9
+ getReadiness(): Promise<{
10
+ healthy: boolean;
11
+ }>;
12
+ getOpenApi(): Promise<OpenAPIObject>;
13
+ getOpenApiHtml(): string;
14
+ getMetrics(): Promise<string>;
15
+ }
@@ -0,0 +1,186 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Controller, Get } from 'routing-controllers';
11
+ import { getMetadataArgsStorage } from 'routing-controllers';
12
+ import { routingControllersToSpec } from 'routing-controllers-openapi';
13
+ import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
14
+ import { ResponseSchema } from 'routing-controllers-openapi';
15
+ import { IsBoolean } from 'class-validator';
16
+ import { getMetrics, health } from '@takaro/util';
17
+ import { PERMISSIONS } from '@takaro/auth';
18
+ import { EventMapping } from '@takaro/modules';
19
+ let spec;
20
+ export class HealthOutputDTO {
21
+ }
22
+ __decorate([
23
+ IsBoolean(),
24
+ __metadata("design:type", Boolean)
25
+ ], HealthOutputDTO.prototype, "healthy", void 0);
26
+ let Meta = class Meta {
27
+ async getHealth() {
28
+ return { healthy: true };
29
+ }
30
+ async getReadiness() {
31
+ const healthy = await health.check();
32
+ return { healthy };
33
+ }
34
+ async getOpenApi() {
35
+ if (spec)
36
+ return spec;
37
+ const { getMetadataStorage } = await import('class-validator');
38
+ const classTransformerStorage = await import(
39
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
40
+ // @ts-ignore were doing an import of internal code and ts doesnt like that
41
+ // But this does work, trust me bro...
42
+ 'class-transformer/cjs/storage.js');
43
+ const storage = getMetadataArgsStorage();
44
+ const schemas = validationMetadatasToSchemas({
45
+ refPointerPrefix: '#/components/schemas/',
46
+ classTransformerMetadataStorage: classTransformerStorage.defaultMetadataStorage,
47
+ classValidatorMetadataStorage: getMetadataStorage(),
48
+ forbidNonWhitelisted: true,
49
+ });
50
+ spec = routingControllersToSpec(storage, {}, {
51
+ components: {
52
+ schemas,
53
+ securitySchemes: {
54
+ adminAuth: {
55
+ description: 'Used for system administration, like creating or deleting domains',
56
+ type: 'http',
57
+ scheme: 'bearer',
58
+ bearerFormat: 'JWT',
59
+ },
60
+ domainAuth: {
61
+ description: 'Used for anything inside a domain. Players, GameServers, etc.',
62
+ type: 'apiKey',
63
+ in: 'cookie',
64
+ name: 'takaro-token',
65
+ },
66
+ },
67
+ },
68
+ });
69
+ // Add required permissions to operation descriptions
70
+ const requiredPermsRegex = /authMiddleware\((.+)\)/;
71
+ storage.uses.forEach((use) => {
72
+ const requiredPerms = use.middleware.name
73
+ .match(requiredPermsRegex)?.[1]
74
+ .split(',')
75
+ .map((p) => `\`${p}\``)
76
+ .join(', ') || [];
77
+ const operationId = `${use.target.name}.${use.method}`;
78
+ if (!requiredPerms.length)
79
+ return;
80
+ // Find the corresponding path and method in spec
81
+ Object.keys(spec?.paths ?? []).forEach((pathKey) => {
82
+ const pathItem = spec?.paths[pathKey];
83
+ Object.keys(pathItem).forEach((method) => {
84
+ const operation = pathItem[method];
85
+ if (operation.operationId === operationId) {
86
+ // Update the description with required permissions
87
+ operation.description = (operation.description || '') + ` Required permissions: ${requiredPerms}`;
88
+ }
89
+ });
90
+ });
91
+ });
92
+ if (spec.components?.schemas) {
93
+ spec.components.schemas.PERMISSIONS = {
94
+ enum: Object.values(PERMISSIONS),
95
+ };
96
+ }
97
+ // Force event meta to be the correct types
98
+ // TODO: figure out how to do this 'properly' with class-validator
99
+ const allEvents = Object.values(EventMapping).map((e) => e.name);
100
+ const eventOutputMetaSchema = spec.components?.schemas?.EventOutputDTO;
101
+ if (eventOutputMetaSchema && 'properties' in eventOutputMetaSchema && eventOutputMetaSchema.properties) {
102
+ eventOutputMetaSchema.properties.meta = {
103
+ oneOf: [...allEvents.map((e) => ({ $ref: `#/components/schemas/${e}` }))],
104
+ };
105
+ }
106
+ return spec;
107
+ }
108
+ getOpenApiHtml() {
109
+ return `<!DOCTYPE html>
110
+ <html>
111
+ <head>
112
+ <meta charset="utf-8" />
113
+ <script
114
+ type="module"
115
+ src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"
116
+ ></script>
117
+ </head>
118
+ <body>
119
+ <rapi-doc
120
+ spec-url="/openapi.json"
121
+ render-style="read"
122
+ fill-request-fields-with-example="false"
123
+ persist-auth="true"
124
+
125
+ sort-tags="true"
126
+ sort-endpoints-by="method"
127
+
128
+ show-method-in-nav-bar="as-colored-block"
129
+ show-header="false"
130
+ allow-authentication="false"
131
+ allow-server-selection="false"
132
+
133
+ schema-style="table"
134
+ schema-expand-level="1"
135
+ default-schema-tab="schema"
136
+
137
+ primary-color="#664de5"
138
+ bg-color="#151515"
139
+ text-color="#c2c2c2"
140
+ header-color="#353535"
141
+ />
142
+ </body>
143
+ </html>
144
+ `;
145
+ }
146
+ getMetrics() {
147
+ return getMetrics();
148
+ }
149
+ };
150
+ __decorate([
151
+ Get('/healthz'),
152
+ ResponseSchema(HealthOutputDTO),
153
+ __metadata("design:type", Function),
154
+ __metadata("design:paramtypes", []),
155
+ __metadata("design:returntype", Promise)
156
+ ], Meta.prototype, "getHealth", null);
157
+ __decorate([
158
+ Get('/readyz'),
159
+ ResponseSchema(HealthOutputDTO),
160
+ __metadata("design:type", Function),
161
+ __metadata("design:paramtypes", []),
162
+ __metadata("design:returntype", Promise)
163
+ ], Meta.prototype, "getReadiness", null);
164
+ __decorate([
165
+ Get('/openapi.json'),
166
+ __metadata("design:type", Function),
167
+ __metadata("design:paramtypes", []),
168
+ __metadata("design:returntype", Promise)
169
+ ], Meta.prototype, "getOpenApi", null);
170
+ __decorate([
171
+ Get('/api.html'),
172
+ __metadata("design:type", Function),
173
+ __metadata("design:paramtypes", []),
174
+ __metadata("design:returntype", void 0)
175
+ ], Meta.prototype, "getOpenApiHtml", null);
176
+ __decorate([
177
+ Get('/metrics'),
178
+ __metadata("design:type", Function),
179
+ __metadata("design:paramtypes", []),
180
+ __metadata("design:returntype", void 0)
181
+ ], Meta.prototype, "getMetrics", null);
182
+ Meta = __decorate([
183
+ Controller()
184
+ ], Meta);
185
+ export { Meta };
186
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/controllers/meta.ts"],"names":[],"mappings":"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"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { HTTP } from './app.js';
2
+ export * from './middleware/metrics.js';
3
+ export { createRateLimitMiddleware } from './middleware/rateLimit.js';
4
+ export { adminAuthMiddleware } from './middleware/adminAuth.js';
5
+ export { paginationMiddleware } from './middleware/paginationMiddleware.js';
6
+ export { apiResponse, APIOutput } from './util/apiResponse.js';
package/dist/main.js ADDED
@@ -0,0 +1,7 @@
1
+ export { HTTP } from './app.js';
2
+ export * from './middleware/metrics.js';
3
+ export { createRateLimitMiddleware } from './middleware/rateLimit.js';
4
+ export { adminAuthMiddleware } from './middleware/adminAuth.js';
5
+ export { paginationMiddleware } from './middleware/paginationMiddleware.js';
6
+ export { apiResponse, APIOutput } from './util/apiResponse.js';
7
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export declare function adminAuthMiddleware(request: Request, response: Response, next: NextFunction): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { errors, logger } from '@takaro/util';
2
+ import { ory, AUDIENCES } from '@takaro/auth';
3
+ const log = logger('http:middleware:adminAuth');
4
+ export async function adminAuthMiddleware(request, response, next) {
5
+ try {
6
+ const rawToken = request.headers['authorization']?.replace('Bearer ', '');
7
+ if (!rawToken) {
8
+ log.warn('No token provided');
9
+ return next(new errors.UnauthorizedError());
10
+ }
11
+ const token = await ory.introspectToken(rawToken);
12
+ if (!token.active) {
13
+ log.warn('Token is not active');
14
+ return next(new errors.ForbiddenError());
15
+ }
16
+ if (!token.aud.includes(AUDIENCES.TAKARO_API_ADMIN)) {
17
+ log.warn('Token is not for admin API', { token });
18
+ return next(new errors.ForbiddenError());
19
+ }
20
+ return next();
21
+ }
22
+ catch (error) {
23
+ log.error('Unexpected error', { error });
24
+ next(new errors.ForbiddenError());
25
+ }
26
+ }
27
+ //# sourceMappingURL=adminAuth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adminAuth.js","sourceRoot":"","sources":["../../src/middleware/adminAuth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,GAAG,GAAG,MAAM,CAAC,2BAA2B,CAAC,CAAC;AAEhD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAgB,EAAE,QAAkB,EAAE,IAAkB;IAChG,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE1E,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,GAAG,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;YAC9B,OAAO,IAAI,CAAC,IAAI,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QAElD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAClB,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;YAChC,OAAO,IAAI,CAAC,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;QAC3C,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACpD,GAAG,CAAC,IAAI,CAAC,4BAA4B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAClD,OAAO,IAAI,CAAC,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;QAC3C,CAAC;QAED,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,KAAK,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;IACpC,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export declare function ErrorHandler(originalError: Error, req: Request, res: Response, _next: NextFunction): Promise<Response<any, Record<string, any>>>;
@@ -0,0 +1,51 @@
1
+ import { HttpError } from 'routing-controllers';
2
+ import { logger, errors } from '@takaro/util';
3
+ import { apiResponse } from '../util/apiResponse.js';
4
+ const log = logger('errorHandler');
5
+ export async function ErrorHandler(originalError, req, res,
6
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
+ _next) {
8
+ let status = 500;
9
+ let parsedError = new errors.InternalServerError();
10
+ if (originalError.name === 'BadRequestError') {
11
+ if (originalError.hasOwnProperty('errors')) {
12
+ // @ts-expect-error Error typing is weird in ts... but we validate during runtime so should be OK
13
+ const validationErrors = originalError['errors'];
14
+ parsedError = new errors.ValidationError('Validation error', validationErrors);
15
+ log.warn('⚠️ Validation errror', { details: validationErrors.map((e) => JSON.stringify(e.target, null, 2)) });
16
+ }
17
+ }
18
+ if (originalError instanceof HttpError) {
19
+ status = originalError.httpCode;
20
+ }
21
+ if (originalError.name === 'UniqueViolationError') {
22
+ status = 409;
23
+ parsedError = new errors.ConflictError('Unique constraint violation');
24
+ }
25
+ if (originalError.name === 'NotNullViolationError') {
26
+ status = 400;
27
+ parsedError = new errors.BadRequestError('Missing required field');
28
+ }
29
+ if (originalError instanceof errors.TakaroError) {
30
+ status = originalError.http;
31
+ parsedError = originalError;
32
+ }
33
+ // If error is a JSON.parse error
34
+ if (originalError instanceof SyntaxError) {
35
+ if (originalError.message.includes('Unexpected token') ||
36
+ originalError.message.includes('Unexpected end of JSON input')) {
37
+ status = 400;
38
+ parsedError = new errors.BadRequestError('Invalid JSON');
39
+ }
40
+ }
41
+ log.error(originalError);
42
+ if (status >= 500) {
43
+ log.error(`🔴 FAIL ${req.method} ${req.originalUrl}`, parsedError);
44
+ }
45
+ else {
46
+ log.warn(`⚠️ FAIL ${req.method} ${req.originalUrl}`, parsedError);
47
+ }
48
+ res.status(status).json(apiResponse({}, { error: parsedError, req, res }));
49
+ return res.end();
50
+ }
51
+ //# sourceMappingURL=errorHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,MAAM,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;AAEnC,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,aAAoB,EACpB,GAAY,EACZ,GAAa;AACb,6DAA6D;AAC7D,KAAmB;IAEnB,IAAI,MAAM,GAAG,GAAG,CAAC;IACjB,IAAI,WAAW,GAAG,IAAI,MAAM,CAAC,mBAAmB,EAAE,CAAC;IAEnD,IAAI,aAAa,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;QAC7C,IAAI,aAAa,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3C,iGAAiG;YACjG,MAAM,gBAAgB,GAAG,aAAa,CAAC,QAAQ,CAAsB,CAAC;YACtE,WAAW,GAAG,IAAI,MAAM,CAAC,eAAe,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,CAAC;YAC/E,GAAG,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,OAAO,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChH,CAAC;IACH,CAAC;IAED,IAAI,aAAa,YAAY,SAAS,EAAE,CAAC;QACvC,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC;IAClC,CAAC;IAED,IAAI,aAAa,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;QAClD,MAAM,GAAG,GAAG,CAAC;QACb,WAAW,GAAG,IAAI,MAAM,CAAC,aAAa,CAAC,6BAA6B,CAAC,CAAC;IACxE,CAAC;IAED,IAAI,aAAa,CAAC,IAAI,KAAK,uBAAuB,EAAE,CAAC;QACnD,MAAM,GAAG,GAAG,CAAC;QACb,WAAW,GAAG,IAAI,MAAM,CAAC,eAAe,CAAC,wBAAwB,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,aAAa,YAAY,MAAM,CAAC,WAAW,EAAE,CAAC;QAChD,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC;QAC5B,WAAW,GAAG,aAAa,CAAC;IAC9B,CAAC;IAED,iCAAiC;IACjC,IAAI,aAAa,YAAY,WAAW,EAAE,CAAC;QACzC,IACE,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;YAClD,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EAC9D,CAAC;YACD,MAAM,GAAG,GAAG,CAAC;YACb,WAAW,GAAG,IAAI,MAAM,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IACzB,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QAClB,GAAG,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,CAAC;IACrE,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,IAAI,CAAC,WAAW,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,CAAC;IACpE,CAAC;IAED,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IAC3E,OAAO,GAAG,CAAC,GAAG,EAAE,CAAC;AACnB,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This middleware is called very early in the request lifecycle, so it's
3
+ * we leverage this fact to inject the context tracking at this stage
4
+ */
5
+ export declare const LoggingMiddleware: (...args: any[]) => unknown;
@@ -0,0 +1,51 @@
1
+ import { logger, ctx } from '@takaro/util';
2
+ const SUPPRESS_BODY_KEYWORDS = ['password', 'newPassword'];
3
+ const HIDDEN_ROUTES = ['/metrics', '/health', '/healthz', '/ready', '/readyz', '/queues/api/queues'];
4
+ import { context, trace } from '@opentelemetry/api';
5
+ const log = logger('http');
6
+ /**
7
+ * This middleware is called very early in the request lifecycle, so it's
8
+ * we leverage this fact to inject the context tracking at this stage
9
+ */
10
+ export const LoggingMiddleware = ctx.wrap('HTTP', loggingMiddleware);
11
+ async function loggingMiddleware(req, res, next) {
12
+ if (HIDDEN_ROUTES.some((route) => req.originalUrl.startsWith(route))) {
13
+ return next();
14
+ }
15
+ const requestStartMs = Date.now();
16
+ const hideData = SUPPRESS_BODY_KEYWORDS.some((keyword) => (JSON.stringify(req.body) || '').includes(keyword));
17
+ log.debug(`⬇️ ${req.method} ${req.originalUrl}`, {
18
+ ip: req.ip,
19
+ method: req.method,
20
+ path: req.originalUrl,
21
+ body: hideData ? { suppressed_output: true } : req.body,
22
+ });
23
+ const span = trace.getSpan(context.active());
24
+ if (span) {
25
+ // get the trace ID from the span and set it in the headers
26
+ const traceId = span.spanContext().traceId;
27
+ res.header('X-Trace-Id', traceId);
28
+ }
29
+ // Log on API Call Finish to add responseTime
30
+ res.once('finish', () => {
31
+ const responseTime = Date.now() - requestStartMs;
32
+ log.info(`⬆️ ${req.method} ${req.originalUrl}`, {
33
+ responseTime,
34
+ requestMethod: req.method,
35
+ requestUrl: req.originalUrl,
36
+ requestSize: req.headers['content-length'],
37
+ status: res.statusCode,
38
+ responseSize: res.getHeader('Content-Length'),
39
+ userAgent: req.get('User-Agent'),
40
+ remoteIp: req.ip,
41
+ serverIp: '127.0.0.1',
42
+ referer: req.get('Referer'),
43
+ cacheLookup: false,
44
+ cacheHit: false,
45
+ cacheValidatedWithOriginServer: false,
46
+ protocol: req.protocol,
47
+ });
48
+ });
49
+ next();
50
+ }
51
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/middleware/logger.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,sBAAsB,GAAG,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAC3D,MAAM,aAAa,GAAG,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;AACrG,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AACpD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;AAE3B;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAErE,KAAK,UAAU,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAC9E,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACrE,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAED,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAElC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAE9G,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,WAAW,EAAE,EAAE;QAC/C,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,IAAI,EAAE,GAAG,CAAC,WAAW;QACrB,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;KACxD,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE7C,IAAI,IAAI,EAAE,CAAC;QACT,2DAA2D;QAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC;QAC3C,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,6CAA6C;IAC7C,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE;QACtB,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,CAAC;QAEjD,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,WAAW,EAAE,EAAE;YAC9C,YAAY;YACZ,aAAa,EAAE,GAAG,CAAC,MAAM;YACzB,UAAU,EAAE,GAAG,CAAC,WAAW;YAC3B,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC;YAC1C,MAAM,EAAE,GAAG,CAAC,UAAU;YACtB,YAAY,EAAE,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC;YAC7C,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;YAChC,QAAQ,EAAE,GAAG,CAAC,EAAE;YAChB,QAAQ,EAAE,WAAW;YACrB,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC;YAC3B,WAAW,EAAE,KAAK;YAClB,QAAQ,EAAE,KAAK;YACf,8BAA8B,EAAE,KAAK;YACrC,QAAQ,EAAE,GAAG,CAAC,QAAQ;SACvB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,EAAE,CAAC;AACT,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export declare function metricsMiddleware(req: Request, res: Response, next: NextFunction): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import { Counter, Histogram } from 'prom-client';
2
+ const counter = new Counter({
3
+ name: 'http_requests_total',
4
+ help: 'Total number of HTTP requests made',
5
+ labelNames: ['path', 'method', 'status'],
6
+ });
7
+ const histogram = new Histogram({
8
+ name: 'http_request_duration_seconds',
9
+ help: 'Duration of HTTP requests in seconds',
10
+ labelNames: ['path', 'method', 'status'],
11
+ buckets: [0.1, 0.5, 1, 2, 5, 10],
12
+ });
13
+ export async function metricsMiddleware(req, res, next) {
14
+ const rawPath = req.path;
15
+ const method = req.method;
16
+ // Filter out anything that looks like a UUID from path
17
+ const path = rawPath.replace(/\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, '/:id');
18
+ const start = Date.now();
19
+ try {
20
+ await next();
21
+ }
22
+ catch (error) {
23
+ throw error;
24
+ }
25
+ finally {
26
+ counter.inc({
27
+ path,
28
+ method,
29
+ status: res.statusCode.toString(),
30
+ });
31
+ histogram.observe({
32
+ path,
33
+ method,
34
+ status: res.statusCode.toString(),
35
+ }, (Date.now() - start) / 1000);
36
+ }
37
+ }
38
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/middleware/metrics.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC;IAC1B,IAAI,EAAE,qBAAqB;IAC3B,IAAI,EAAE,oCAAoC;IAC1C,UAAU,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC;CACzC,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC;IAC9B,IAAI,EAAE,+BAA+B;IACrC,IAAI,EAAE,sCAAsC;IAC5C,UAAU,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACxC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACrF,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC;IACzB,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IAC1B,uDAAuD;IACvD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,iEAAiE,EAAE,MAAM,CAAC,CAAC;IAExG,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEzB,IAAI,CAAC;QACH,MAAM,IAAI,EAAE,CAAC;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,GAAG,CAAC;YACV,IAAI;YACJ,MAAM;YACN,MAAM,EAAE,GAAG,CAAC,UAAU,CAAC,QAAQ,EAAE;SAClC,CAAC,CAAC;QACH,SAAS,CAAC,OAAO,CACf;YACE,IAAI;YACJ,MAAM;YACN,MAAM,EAAE,GAAG,CAAC,UAAU,CAAC,QAAQ,EAAE;SAClC,EACD,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,IAAI,CAC5B,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { NextFunction, Request, Response } from 'express';
2
+ export declare function paginationMiddleware(req: Request, res: Response, next: NextFunction): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import * as yup from 'yup';
2
+ import { errors, logger } from '@takaro/util';
3
+ const log = logger('http:pagination');
4
+ const paginationSchema = yup.object({
5
+ page: yup.number().default(0).min(0),
6
+ limit: yup.number().default(100).min(1).max(1000),
7
+ });
8
+ export async function paginationMiddleware(req, res, next) {
9
+ const merged = { ...req.query, ...req.body };
10
+ try {
11
+ const result = await paginationSchema.validate(merged);
12
+ res.locals.page = result.page;
13
+ res.locals.limit = result.limit;
14
+ next();
15
+ }
16
+ catch (error) {
17
+ if (error instanceof yup.ValidationError) {
18
+ next(new errors.ValidationError('Invalid pagination', [error]));
19
+ }
20
+ else {
21
+ log.error('Unexpected error', error);
22
+ next(new errors.InternalServerError());
23
+ }
24
+ }
25
+ }
26
+ //# sourceMappingURL=paginationMiddleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paginationMiddleware.js","sourceRoot":"","sources":["../../src/middleware/paginationMiddleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,GAAG,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAEtC,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;CAClD,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACxF,MAAM,MAAM,GAAG,EAAE,GAAG,GAAG,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC7C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAEvD,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QAC9B,GAAG,CAAC,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAEhC,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,GAAG,CAAC,eAAe,EAAE,CAAC;YACzC,IAAI,CAAC,IAAI,MAAM,CAAC,eAAe,CAAC,oBAAoB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAClE,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;YACrC,IAAI,CAAC,IAAI,MAAM,CAAC,mBAAmB,EAAE,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export interface IRateLimitMiddlewareOptions {
3
+ max: number;
4
+ windowSeconds: number;
5
+ keyPrefix?: string;
6
+ useInMemory?: boolean;
7
+ }
8
+ export declare function createRateLimitMiddleware(opts: IRateLimitMiddlewareOptions): Promise<(req: Request, res: Response, next: NextFunction) => Promise<void>>;