@biorate/nestjs-tools 1.24.1 → 1.24.2
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/CHANGELOG.md +11 -0
- package/package.json +2 -2
- package/src/decorators/auth.ts +14 -0
- package/src/decorators/index.ts +2 -0
- package/src/decorators/roles.ts +13 -0
- package/src/enums/index.ts +1 -0
- package/src/enums/user.ts +4 -0
- package/src/filters/all-exception-filter.ts +47 -0
- package/src/filters/index.ts +1 -0
- package/src/index.ts +6 -0
- package/src/interceptors/index.ts +1 -0
- package/src/interceptors/routes-interceptor.ts +30 -0
- package/src/middlewares/index.ts +3 -0
- package/src/middlewares/proxy-middleware.ts +69 -0
- package/src/middlewares/request-count-middleware.ts +29 -0
- package/src/middlewares/response-time-middleware.ts +53 -0
- package/src/providers/auth.ts +36 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/roles.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.24.2](https://github.com/biorate/core/compare/v1.24.1...v1.24.2) (2023-02-02)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **nestjs-tools:** types ([483830d](https://github.com/biorate/core/commit/483830dee914458b547502e69134f99caa54317d))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
## [1.24.1](https://github.com/biorate/core/compare/v1.24.0...v1.24.1) (2023-02-02)
|
|
7
18
|
|
|
8
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biorate/nestjs-tools",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.2",
|
|
4
4
|
"description": "NestJS tools",
|
|
5
5
|
"main": "dist",
|
|
6
6
|
"scripts": {
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"@types/express": "^4.17.16",
|
|
41
41
|
"@types/on-headers": "^1.0.0"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "a092d680f5d8df4473a8f07ed038b2fabe20f9a9"
|
|
44
44
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { UseGuards } from '@nestjs/common';
|
|
2
|
+
import { ApiBasicAuth } from '@nestjs/swagger';
|
|
3
|
+
import { AuthGuardProvider } from '../providers';
|
|
4
|
+
|
|
5
|
+
export const AuthBasic =
|
|
6
|
+
() =>
|
|
7
|
+
(
|
|
8
|
+
target: Record<string | symbol, unknown>,
|
|
9
|
+
propertyKey?: string | symbol,
|
|
10
|
+
descriptor?: TypedPropertyDescriptor<unknown>,
|
|
11
|
+
) => {
|
|
12
|
+
ApiBasicAuth()(target, propertyKey!, descriptor!);
|
|
13
|
+
UseGuards(AuthGuardProvider)(target, propertyKey!, descriptor!);
|
|
14
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SetMetadata } from '@nestjs/common';
|
|
2
|
+
import { UserRoles } from '../enums';
|
|
3
|
+
|
|
4
|
+
export const ROLES_KEY = Symbol('RolesKey');
|
|
5
|
+
|
|
6
|
+
export const Roles = (...roles: UserRoles[]) =>
|
|
7
|
+
SetMetadata(
|
|
8
|
+
ROLES_KEY,
|
|
9
|
+
roles.reduce((memo, item) => {
|
|
10
|
+
memo |= item;
|
|
11
|
+
return memo;
|
|
12
|
+
}, 0),
|
|
13
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './user';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BaseError } from '@biorate/errors';
|
|
2
|
+
import {
|
|
3
|
+
ExceptionFilter,
|
|
4
|
+
Catch,
|
|
5
|
+
ArgumentsHost,
|
|
6
|
+
HttpException,
|
|
7
|
+
HttpStatus,
|
|
8
|
+
BadRequestException,
|
|
9
|
+
} from '@nestjs/common';
|
|
10
|
+
|
|
11
|
+
@Catch()
|
|
12
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
13
|
+
public catch(exception: unknown, host: ArgumentsHost) {
|
|
14
|
+
const ctx = host.switchToHttp();
|
|
15
|
+
const response = ctx.getResponse();
|
|
16
|
+
const request = ctx.getRequest();
|
|
17
|
+
const status =
|
|
18
|
+
exception instanceof HttpException
|
|
19
|
+
? exception.getStatus()
|
|
20
|
+
: (exception as BaseError)?.meta?.status ?? HttpStatus.INTERNAL_SERVER_ERROR;
|
|
21
|
+
this.log(exception);
|
|
22
|
+
response.status(status).json({
|
|
23
|
+
status,
|
|
24
|
+
code: this.code(exception),
|
|
25
|
+
hint: this.hint(exception),
|
|
26
|
+
path: request.url,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private code(exception: unknown) {
|
|
31
|
+
return (<BaseError>exception).code ?? (<Error>exception).constructor.name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private hint(exception: unknown) {
|
|
35
|
+
return (
|
|
36
|
+
(
|
|
37
|
+
(<BadRequestException>exception)?.getResponse?.() as unknown as {
|
|
38
|
+
message?: [string];
|
|
39
|
+
}
|
|
40
|
+
)?.message ?? (<BaseError>exception).message
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private log(exception: unknown) {
|
|
45
|
+
console.error(exception);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './all-exception-filter';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './routes-interceptor';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { join, normalize } from 'path/posix';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { PATH_METADATA } from '@nestjs/common/constants';
|
|
4
|
+
import { Request } from 'express';
|
|
5
|
+
import {
|
|
6
|
+
Injectable,
|
|
7
|
+
ExecutionContext,
|
|
8
|
+
CallHandler,
|
|
9
|
+
NestInterceptor,
|
|
10
|
+
} from '@nestjs/common';
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class RoutesInterceptor implements NestInterceptor {
|
|
14
|
+
public static readonly map = new WeakMap<Request, { path: string }>();
|
|
15
|
+
|
|
16
|
+
private readonly reflector = new Reflector();
|
|
17
|
+
|
|
18
|
+
public intercept(context: ExecutionContext, next: CallHandler) {
|
|
19
|
+
RoutesInterceptor.map.set(context.switchToHttp().getRequest(), {
|
|
20
|
+
path: normalize(
|
|
21
|
+
join(
|
|
22
|
+
'/',
|
|
23
|
+
this.reflector.get<string>(PATH_METADATA, context.getClass()),
|
|
24
|
+
this.reflector.get<string>(PATH_METADATA, context.getHandler()),
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
});
|
|
28
|
+
return next.handle();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import * as httpProxy from 'http-proxy';
|
|
3
|
+
import { pick, omit } from 'lodash';
|
|
4
|
+
import { Request, Response } from 'express';
|
|
5
|
+
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
6
|
+
import { Options, RequestHandler } from 'http-proxy-middleware/dist/types';
|
|
7
|
+
import { Injectable } from '@nestjs/common';
|
|
8
|
+
import { counter, histogram, Counter, Histogram } from '@biorate/prometheus';
|
|
9
|
+
import { time } from '@biorate/tools';
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class ProxyPrometheusMiddleware {
|
|
13
|
+
@counter({
|
|
14
|
+
name: 'http_proxy_server_requests_seconds_count',
|
|
15
|
+
help: 'Http proxy server requests count',
|
|
16
|
+
labelNames: ['method', 'url', 'status'],
|
|
17
|
+
})
|
|
18
|
+
private counter: Counter;
|
|
19
|
+
|
|
20
|
+
@histogram({
|
|
21
|
+
name: 'http_proxy_server_requests_seconds',
|
|
22
|
+
help: 'Http proxy server requests seconds bucket',
|
|
23
|
+
labelNames: ['method', 'url', 'status'],
|
|
24
|
+
buckets: [5, 10, 20, 50, 100, 300, 500, 1000, 2000, 3000, 5000, 10000],
|
|
25
|
+
})
|
|
26
|
+
private histogram: Histogram;
|
|
27
|
+
|
|
28
|
+
private handler: RequestHandler;
|
|
29
|
+
|
|
30
|
+
private time = new WeakMap<Request, { time: () => number }>();
|
|
31
|
+
|
|
32
|
+
public static create(options?: Options) {
|
|
33
|
+
return new this(options).handler;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private constructor(options?: Options) {
|
|
37
|
+
const { onProxyReq, onProxyRes } = pick(options, 'onProxyReq', 'onProxyRes');
|
|
38
|
+
this.handler = createProxyMiddleware({
|
|
39
|
+
...omit(options, 'onProxyReq', 'onProxyRes'),
|
|
40
|
+
onProxyReq: (
|
|
41
|
+
proxyReq: http.ClientRequest,
|
|
42
|
+
req: Request,
|
|
43
|
+
res: Response,
|
|
44
|
+
options: httpProxy.ServerOptions,
|
|
45
|
+
) => {
|
|
46
|
+
this.time.set(req, { time: time.diff() });
|
|
47
|
+
return onProxyReq?.(proxyReq, req, res, options);
|
|
48
|
+
},
|
|
49
|
+
onProxyRes: (proxyRes: http.IncomingMessage, req: Request, res: Response) => {
|
|
50
|
+
const diff = this.time.get(req);
|
|
51
|
+
this.counter
|
|
52
|
+
.labels({
|
|
53
|
+
method: req.method,
|
|
54
|
+
url: req.baseUrl,
|
|
55
|
+
status: res.statusCode,
|
|
56
|
+
})
|
|
57
|
+
.inc();
|
|
58
|
+
this.histogram
|
|
59
|
+
.labels({
|
|
60
|
+
method: req.method,
|
|
61
|
+
url: req.baseUrl,
|
|
62
|
+
status: res.statusCode,
|
|
63
|
+
})
|
|
64
|
+
.observe(diff!.time());
|
|
65
|
+
return onProxyRes?.(proxyRes, req, res);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { counter, Counter } from '@biorate/prometheus';
|
|
3
|
+
import { Request, Response, NextFunction } from 'express';
|
|
4
|
+
import onHeaders from 'on-headers';
|
|
5
|
+
import { RoutesInterceptor } from '../interceptors';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class RequestCountMiddleware implements NestMiddleware {
|
|
9
|
+
@counter({
|
|
10
|
+
name: 'http_server_requests_seconds_count',
|
|
11
|
+
help: 'Http server requests count',
|
|
12
|
+
labelNames: ['method', 'url', 'status'],
|
|
13
|
+
})
|
|
14
|
+
private counter: Counter;
|
|
15
|
+
|
|
16
|
+
public use(req: Request, res: Response, next: NextFunction) {
|
|
17
|
+
onHeaders(res, () => {
|
|
18
|
+
const route = RoutesInterceptor.map.get(req);
|
|
19
|
+
this.counter
|
|
20
|
+
.labels({
|
|
21
|
+
method: req.method,
|
|
22
|
+
url: route?.path ?? (req.baseUrl || req.originalUrl),
|
|
23
|
+
status: res.statusCode,
|
|
24
|
+
})
|
|
25
|
+
.inc();
|
|
26
|
+
});
|
|
27
|
+
next();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { inject, Types } from '@biorate/inversion';
|
|
3
|
+
import { IConfig } from '@biorate/config';
|
|
4
|
+
import { histogram, Histogram } from '@biorate/prometheus';
|
|
5
|
+
import { Request, Response, NextFunction } from 'express';
|
|
6
|
+
import onHeaders from 'on-headers';
|
|
7
|
+
import { RoutesInterceptor } from '../interceptors';
|
|
8
|
+
import { time } from '@biorate/tools';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class ResponseTimeMiddleware implements NestMiddleware {
|
|
12
|
+
@inject(Types.Config) private config: IConfig;
|
|
13
|
+
|
|
14
|
+
private readonly suffix: boolean;
|
|
15
|
+
|
|
16
|
+
private readonly header: string;
|
|
17
|
+
|
|
18
|
+
private readonly digits: number;
|
|
19
|
+
|
|
20
|
+
@histogram({
|
|
21
|
+
name: 'http_server_requests_seconds',
|
|
22
|
+
help: 'Http server requests seconds bucket',
|
|
23
|
+
labelNames: ['method', 'url', 'status'],
|
|
24
|
+
buckets: [5, 10, 20, 50, 100, 300, 500, 1000, 2000, 3000, 5000, 10000],
|
|
25
|
+
})
|
|
26
|
+
private histogram: Histogram;
|
|
27
|
+
|
|
28
|
+
public constructor() {
|
|
29
|
+
this.suffix = this.config.get<boolean>('app.middleware.ResponseTime.suffix', true);
|
|
30
|
+
this.header = this.config.get<string>(
|
|
31
|
+
'app.middleware.ResponseTime.header',
|
|
32
|
+
'x-response-time',
|
|
33
|
+
);
|
|
34
|
+
this.digits = this.config.get<number>('app.middleware.ResponseTime.digits', 3);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public use(req: Request, res: Response, next: NextFunction) {
|
|
38
|
+
const diff = time.diff();
|
|
39
|
+
onHeaders(res, () => {
|
|
40
|
+
const time = diff();
|
|
41
|
+
const route = RoutesInterceptor.map.get(req);
|
|
42
|
+
this.histogram
|
|
43
|
+
.labels({
|
|
44
|
+
method: req.method,
|
|
45
|
+
url: route?.path ?? (req.baseUrl || req.originalUrl),
|
|
46
|
+
status: res.statusCode,
|
|
47
|
+
})
|
|
48
|
+
.observe(time);
|
|
49
|
+
res.setHeader(this.header, time.toFixed(this.digits) + (this.suffix ? 'ms' : ''));
|
|
50
|
+
});
|
|
51
|
+
next();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { inject, Types } from '@biorate/inversion';
|
|
2
|
+
import { IConfig } from '@biorate/config';
|
|
3
|
+
import {
|
|
4
|
+
Injectable,
|
|
5
|
+
CanActivate,
|
|
6
|
+
ExecutionContext,
|
|
7
|
+
UnauthorizedException,
|
|
8
|
+
} from '@nestjs/common';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class AuthGuardProvider implements CanActivate {
|
|
12
|
+
@inject(Types.Config) private config: IConfig;
|
|
13
|
+
|
|
14
|
+
public canActivate(context: ExecutionContext): boolean {
|
|
15
|
+
const request = context.switchToHttp().getRequest();
|
|
16
|
+
const config = this.config.get<{ [key: string]: string } | void>(
|
|
17
|
+
'app.auth.basic',
|
|
18
|
+
undefined,
|
|
19
|
+
);
|
|
20
|
+
if (!config) return true;
|
|
21
|
+
let username: string, password: string;
|
|
22
|
+
try {
|
|
23
|
+
[username, password] = Buffer.from(
|
|
24
|
+
request.headers.authorization.replace('Basic ', ''),
|
|
25
|
+
'base64',
|
|
26
|
+
)
|
|
27
|
+
.toString('utf-8')
|
|
28
|
+
.split(':');
|
|
29
|
+
} catch {
|
|
30
|
+
throw new UnauthorizedException();
|
|
31
|
+
}
|
|
32
|
+
if (!username || !password) throw new UnauthorizedException();
|
|
33
|
+
if (config[username] === password) return true;
|
|
34
|
+
throw new UnauthorizedException();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { ROLES_KEY } from '../decorators';
|
|
4
|
+
import { UserRoles } from '../enums';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class RolesGuardProvider implements CanActivate {
|
|
8
|
+
constructor(private reflector: Reflector) {}
|
|
9
|
+
/**
|
|
10
|
+
* @description - This method for override
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const request = context.switchToHttp().getRequest();
|
|
14
|
+
* const { login, sessionId } = request.cookies;
|
|
15
|
+
* if (!login || !sessionId) throw new AuthorizationNotAuthorizedError();
|
|
16
|
+
* // get user from db, or somewhere else
|
|
17
|
+
* if (!user) throw new AuthorizationNotAuthorizedError();
|
|
18
|
+
* return user.role;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
protected async getUser(context: ExecutionContext) {
|
|
22
|
+
return { role: UserRoles.Basic | UserRoles.Admin };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async canActivate(context: ExecutionContext) {
|
|
26
|
+
const roles = this.reflector.getAllAndOverride<number>(ROLES_KEY, [
|
|
27
|
+
context.getHandler(),
|
|
28
|
+
context.getClass(),
|
|
29
|
+
]);
|
|
30
|
+
if (roles === undefined) return true;
|
|
31
|
+
const user = await this.getUser(context);
|
|
32
|
+
return (user.role & roles) === roles;
|
|
33
|
+
}
|
|
34
|
+
}
|