@takaro/http 0.0.0-next.0da151e
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 +7 -0
- package/dist/app.d.ts +21 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +86 -0
- package/dist/app.js.map +1 -0
- package/dist/controllers/meta.d.ts +18 -0
- package/dist/controllers/meta.d.ts.map +1 -0
- package/dist/controllers/meta.js +284 -0
- package/dist/controllers/meta.js.map +1 -0
- package/dist/main.d.ts +8 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +8 -0
- package/dist/main.js.map +1 -0
- package/dist/middleware/basicAuth.d.ts +3 -0
- package/dist/middleware/basicAuth.d.ts.map +1 -0
- package/dist/middleware/basicAuth.js +27 -0
- package/dist/middleware/basicAuth.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +3 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +73 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/logger.d.ts +9 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +51 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/metrics.d.ts +3 -0
- package/dist/middleware/metrics.d.ts.map +1 -0
- package/dist/middleware/metrics.js +35 -0
- package/dist/middleware/metrics.js.map +1 -0
- package/dist/middleware/paginationMiddleware.d.ts +3 -0
- package/dist/middleware/paginationMiddleware.d.ts.map +1 -0
- package/dist/middleware/paginationMiddleware.js +26 -0
- package/dist/middleware/paginationMiddleware.js.map +1 -0
- package/dist/middleware/rateLimit.d.ts +9 -0
- package/dist/middleware/rateLimit.d.ts.map +1 -0
- package/dist/middleware/rateLimit.js +60 -0
- package/dist/middleware/rateLimit.js.map +1 -0
- package/dist/util/apiResponse.d.ts +37 -0
- package/dist/util/apiResponse.d.ts.map +1 -0
- package/dist/util/apiResponse.js +99 -0
- package/dist/util/apiResponse.js.map +1 -0
- package/package.json +15 -0
- package/src/app.ts +105 -0
- package/src/controllers/__tests__/meta.integration.test.ts +23 -0
- package/src/controllers/defaultMetadatastorage.d.ts +5 -0
- package/src/controllers/meta.ts +268 -0
- package/src/main.ts +8 -0
- package/src/middleware/__tests__/paginationMiddleware.unit.test.ts +49 -0
- package/src/middleware/__tests__/rateLimit.integration.test.ts +130 -0
- package/src/middleware/basicAuth.ts +34 -0
- package/src/middleware/errorHandler.ts +95 -0
- package/src/middleware/logger.ts +62 -0
- package/src/middleware/metrics.ts +42 -0
- package/src/middleware/paginationMiddleware.ts +29 -0
- package/src/middleware/rateLimit.ts +73 -0
- package/src/util/apiResponse.ts +112 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +8 -0
- package/typedoc.json +3 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from 'express';
|
|
2
|
+
import * as yup from 'yup';
|
|
3
|
+
import { errors, logger } from '@takaro/util';
|
|
4
|
+
|
|
5
|
+
const log = logger('http:pagination');
|
|
6
|
+
|
|
7
|
+
const paginationSchema = yup.object({
|
|
8
|
+
page: yup.number().default(0).min(0),
|
|
9
|
+
limit: yup.number().default(100).min(1).max(1000),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export async function paginationMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
|
|
13
|
+
const merged = { ...req.query, ...req.body };
|
|
14
|
+
try {
|
|
15
|
+
const result = await paginationSchema.validate(merged);
|
|
16
|
+
|
|
17
|
+
res.locals.page = result.page;
|
|
18
|
+
res.locals.limit = result.limit;
|
|
19
|
+
|
|
20
|
+
next();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error instanceof yup.ValidationError) {
|
|
23
|
+
next(new errors.ValidationError('Invalid pagination', [error]));
|
|
24
|
+
} else {
|
|
25
|
+
log.error('Unexpected error', error);
|
|
26
|
+
next(new errors.InternalServerError());
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { Redis } from '@takaro/db';
|
|
3
|
+
import { logger, errors, ctx } from '@takaro/util';
|
|
4
|
+
import { RateLimiterRes, RateLimiterRedis, RateLimiterMemory } from 'rate-limiter-flexible';
|
|
5
|
+
|
|
6
|
+
export interface IRateLimitMiddlewareOptions {
|
|
7
|
+
max: number;
|
|
8
|
+
windowSeconds: number;
|
|
9
|
+
keyPrefix?: string;
|
|
10
|
+
useInMemory?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function createRateLimitMiddleware(opts: IRateLimitMiddlewareOptions) {
|
|
14
|
+
const log = logger('http:rateLimit');
|
|
15
|
+
const redis = await Redis.getClient('http:rateLimit');
|
|
16
|
+
|
|
17
|
+
// We create a randomHash to use in Redis keys
|
|
18
|
+
// This makes sure that each endpoint can get different rate limits without too much hassle
|
|
19
|
+
const randomHash = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
20
|
+
|
|
21
|
+
let rateLimiter: RateLimiterMemory | RateLimiterRedis;
|
|
22
|
+
|
|
23
|
+
if (opts.useInMemory) {
|
|
24
|
+
rateLimiter = new RateLimiterMemory({
|
|
25
|
+
points: opts.max,
|
|
26
|
+
duration: opts.windowSeconds,
|
|
27
|
+
keyPrefix: `http:rateLimit:${opts.keyPrefix ? opts.keyPrefix : randomHash}`,
|
|
28
|
+
});
|
|
29
|
+
} else {
|
|
30
|
+
rateLimiter = new RateLimiterRedis({
|
|
31
|
+
points: opts.max,
|
|
32
|
+
duration: opts.windowSeconds,
|
|
33
|
+
storeClient: redis,
|
|
34
|
+
keyPrefix: `http:rateLimit:${opts.keyPrefix ? opts.keyPrefix : randomHash}`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
39
|
+
const ctxData = ctx.data;
|
|
40
|
+
let limitedKey = null;
|
|
41
|
+
if (ctxData.user) {
|
|
42
|
+
limitedKey = ctxData.user;
|
|
43
|
+
} else {
|
|
44
|
+
// TODO: should handle case ip is undefined
|
|
45
|
+
limitedKey = req.ip!;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let rateLimiterRes: RateLimiterRes | null = null;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
rateLimiterRes = await rateLimiter.consume(limitedKey);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err instanceof RateLimiterRes) {
|
|
54
|
+
rateLimiterRes = err;
|
|
55
|
+
log.warn(`rate limited, try again in ${err.msBeforeNext}ms`);
|
|
56
|
+
} else {
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (rateLimiterRes) {
|
|
62
|
+
res.set('X-RateLimit-Limit', opts.max.toString());
|
|
63
|
+
res.set('X-RateLimit-Remaining', rateLimiterRes.remainingPoints.toString());
|
|
64
|
+
res.set('X-RateLimit-Reset', rateLimiterRes.msBeforeNext.toString());
|
|
65
|
+
|
|
66
|
+
if (rateLimiterRes.remainingPoints === 0) {
|
|
67
|
+
next(new errors.TooManyRequestsError());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return next();
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { errors, isTakaroDTO, TakaroDTO } from '@takaro/util';
|
|
2
|
+
import { Type } from 'class-transformer';
|
|
3
|
+
import {
|
|
4
|
+
IsISO8601,
|
|
5
|
+
IsNumber,
|
|
6
|
+
IsOptional,
|
|
7
|
+
ValidateNested,
|
|
8
|
+
IsString,
|
|
9
|
+
ValidationError as ClassValidatorError,
|
|
10
|
+
} from 'class-validator';
|
|
11
|
+
import { Request, Response } from 'express';
|
|
12
|
+
|
|
13
|
+
class ErrorOutput {
|
|
14
|
+
@IsString()
|
|
15
|
+
code?: string;
|
|
16
|
+
|
|
17
|
+
@IsString()
|
|
18
|
+
message?: string;
|
|
19
|
+
|
|
20
|
+
@IsString()
|
|
21
|
+
details?: string | ClassValidatorError[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class MetadataOutput {
|
|
25
|
+
@IsString()
|
|
26
|
+
@IsISO8601()
|
|
27
|
+
serverTime!: string;
|
|
28
|
+
|
|
29
|
+
@Type(() => ErrorOutput)
|
|
30
|
+
@ValidateNested()
|
|
31
|
+
error?: ErrorOutput;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The page number of the response
|
|
35
|
+
*/
|
|
36
|
+
@IsNumber()
|
|
37
|
+
@IsOptional()
|
|
38
|
+
page?: number;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The number of items returned in the response (aka page size)
|
|
42
|
+
*/
|
|
43
|
+
@IsNumber()
|
|
44
|
+
@IsOptional()
|
|
45
|
+
limit?: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The total number of items in the collection
|
|
49
|
+
*/
|
|
50
|
+
@IsNumber()
|
|
51
|
+
@IsOptional()
|
|
52
|
+
total?: number;
|
|
53
|
+
}
|
|
54
|
+
export class APIOutput<T> extends TakaroDTO<APIOutput<T>> {
|
|
55
|
+
@Type(() => MetadataOutput)
|
|
56
|
+
@ValidateNested()
|
|
57
|
+
meta!: MetadataOutput;
|
|
58
|
+
|
|
59
|
+
data!: T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface IApiResponseOptions {
|
|
63
|
+
error?: Error;
|
|
64
|
+
meta?: Record<string, string | number>;
|
|
65
|
+
req: Request;
|
|
66
|
+
res: Response;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function apiResponse(data: unknown = {}, opts?: IApiResponseOptions): APIOutput<unknown> {
|
|
70
|
+
const returnVal = new APIOutput<unknown>();
|
|
71
|
+
|
|
72
|
+
returnVal.meta = new MetadataOutput();
|
|
73
|
+
returnVal.data = {};
|
|
74
|
+
|
|
75
|
+
if (opts?.error) {
|
|
76
|
+
returnVal.meta.error = new ErrorOutput();
|
|
77
|
+
|
|
78
|
+
returnVal.meta.error.code = String(opts.error.name);
|
|
79
|
+
|
|
80
|
+
if ('details' in opts.error) {
|
|
81
|
+
if (opts.error instanceof errors.ValidationError) {
|
|
82
|
+
returnVal.meta.error.details = opts.error.details as ClassValidatorError[];
|
|
83
|
+
} else {
|
|
84
|
+
returnVal.meta.error.details = opts.error.details as string;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
returnVal.meta.error.message = String(opts.error.message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (opts?.meta) {
|
|
92
|
+
returnVal.meta.page = opts?.res.locals.page;
|
|
93
|
+
returnVal.meta.limit = opts?.res.locals.limit;
|
|
94
|
+
returnVal.meta.total = opts?.meta.total as number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isTakaroDTO(data)) {
|
|
98
|
+
returnVal.data = data.toJSON();
|
|
99
|
+
} else if (Array.isArray(data)) {
|
|
100
|
+
returnVal.data = data.map((item) => {
|
|
101
|
+
if (isTakaroDTO(item)) {
|
|
102
|
+
return item.toJSON();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return item;
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
returnVal.data = data;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return returnVal;
|
|
112
|
+
}
|
package/tsconfig.json
ADDED
package/typedoc.json
ADDED