nestforge-generator 0.3.0 → 0.4.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 +13 -1
- package/README.pt-BR.md +13 -1
- package/dist/features/auth-strategy.js +7 -7
- package/dist/features/dependencies.js +3 -3
- package/dist/features/language.js +1 -1
- package/dist/features/language.js.map +1 -1
- package/dist/features/markers.js +11 -11
- package/dist/features/markers.js.map +1 -1
- package/dist/generator.js +7 -1
- package/dist/generator.js.map +1 -1
- package/dist/project-name.js +50 -0
- package/dist/project-name.js.map +1 -0
- package/dist/prompts.js +12 -10
- package/dist/prompts.js.map +1 -1
- package/package.json +1 -1
- package/templates/drizzle/drizzle.config.ts +2 -2
- package/templates/drizzle/src/auth/auth.controller.ts +35 -35
- package/templates/drizzle/src/auth/auth.service.spec.ts +4 -4
- package/templates/drizzle/src/auth/auth.service.ts +9 -9
- package/templates/drizzle/src/auth/dto/forgot-password.dto.ts +2 -2
- package/templates/drizzle/src/auth/dto/login.dto.ts +3 -3
- package/templates/drizzle/src/auth/dto/register.dto.ts +4 -4
- package/templates/drizzle/src/auth/dto/reset-password.dto.ts +3 -3
- package/templates/drizzle/src/auth/guards/github-auth.guard.ts +2 -2
- package/templates/drizzle/src/auth/guards/google-auth.guard.ts +2 -2
- package/templates/drizzle/src/auth/guards/session-auth.guard.spec.ts +4 -4
- package/templates/drizzle/src/auth/guards/session-auth.guard.ts +2 -2
- package/templates/drizzle/src/auth/session.service.spec.ts +5 -5
- package/templates/drizzle/src/auth/session.service.ts +5 -5
- package/templates/drizzle/src/auth/strategies/github.strategy.ts +3 -3
- package/templates/drizzle/src/auth/token.service.ts +3 -3
- package/templates/drizzle/src/common/filters/http-exception.filter.ts +1 -1
- package/templates/drizzle/src/common/guards/permissions.guard.spec.ts +7 -7
- package/templates/drizzle/src/common/guards/roles.guard.spec.ts +4 -4
- package/templates/drizzle/src/common/middleware/csrf.middleware.spec.ts +5 -5
- package/templates/drizzle/src/common/middleware/csrf.middleware.ts +2 -2
- package/templates/drizzle/src/common/utils/avatar-storage.util.ts +2 -2
- package/templates/drizzle/src/config/env.validation.ts +1 -1
- package/templates/drizzle/src/database/seed.ts +4 -4
- package/templates/drizzle/src/health/health.controller.ts +2 -2
- package/templates/drizzle/src/health/indicators/drizzle-health.indicator.spec.ts +5 -5
- package/templates/drizzle/src/health/indicators/drizzle-health.indicator.ts +2 -2
- package/templates/drizzle/src/health/indicators/redis-health.indicator.spec.ts +5 -5
- package/templates/drizzle/src/health/indicators/redis-health.indicator.ts +3 -3
- package/templates/drizzle/src/mail/mail.processor.ts +4 -4
- package/templates/drizzle/src/mail/templates/email-templates.ts +7 -7
- package/templates/drizzle/src/metrics/metrics.service.ts +3 -3
- package/templates/drizzle/src/users/dto/create-user.dto.ts +5 -5
- package/templates/drizzle/src/users/dto/find-users-query.dto.ts +3 -3
- package/templates/drizzle/src/users/users.controller.ts +21 -21
- package/templates/drizzle/src/users/users.service.spec.ts +12 -12
- package/templates/drizzle/src/users/users.service.ts +4 -4
- package/templates/drizzle/test/auth.e2e-spec.ts +8 -8
- package/templates/drizzle/test/session-auth.e2e-spec.ts +9 -9
- package/templates/drizzle/test/users.e2e-spec.ts +18 -18
- package/templates/prisma/prisma/seed.ts +46 -46
- package/templates/prisma/src/auth/auth.controller.ts +35 -35
- package/templates/prisma/src/auth/auth.service.spec.ts +3 -3
- package/templates/prisma/src/auth/auth.service.ts +11 -11
- package/templates/prisma/src/auth/dto/forgot-password.dto.ts +2 -2
- package/templates/prisma/src/auth/dto/login.dto.ts +3 -3
- package/templates/prisma/src/auth/dto/register.dto.ts +4 -4
- package/templates/prisma/src/auth/dto/reset-password.dto.ts +3 -3
- package/templates/prisma/src/auth/guards/github-auth.guard.ts +10 -10
- package/templates/prisma/src/auth/guards/google-auth.guard.ts +10 -10
- package/templates/prisma/src/auth/guards/session-auth.guard.spec.ts +4 -4
- package/templates/prisma/src/auth/guards/session-auth.guard.ts +2 -2
- package/templates/prisma/src/auth/session.service.spec.ts +5 -5
- package/templates/prisma/src/auth/session.service.ts +5 -5
- package/templates/prisma/src/auth/strategies/github.strategy.ts +3 -3
- package/templates/prisma/src/auth/token.service.ts +2 -2
- package/templates/prisma/src/common/filters/http-exception.filter.ts +34 -34
- package/templates/prisma/src/common/guards/permissions.guard.spec.ts +7 -7
- package/templates/prisma/src/common/guards/roles.guard.spec.ts +4 -4
- package/templates/prisma/src/common/middleware/csrf.middleware.spec.ts +5 -5
- package/templates/prisma/src/common/middleware/csrf.middleware.ts +2 -2
- package/templates/prisma/src/common/utils/avatar-storage.util.ts +32 -32
- package/templates/prisma/src/config/env.validation.ts +1 -1
- package/templates/prisma/src/health/health.controller.ts +2 -2
- package/templates/prisma/src/health/indicators/prisma-health.indicator.spec.ts +4 -4
- package/templates/prisma/src/health/indicators/prisma-health.indicator.ts +1 -1
- package/templates/prisma/src/health/indicators/redis-health.indicator.spec.ts +5 -5
- package/templates/prisma/src/health/indicators/redis-health.indicator.ts +3 -3
- package/templates/prisma/src/mail/mail.processor.ts +4 -4
- package/templates/prisma/src/mail/templates/email-templates.ts +7 -7
- package/templates/prisma/src/metrics/metrics.service.ts +34 -34
- package/templates/prisma/src/users/dto/create-user.dto.ts +12 -12
- package/templates/prisma/src/users/dto/find-users-query.dto.ts +12 -12
- package/templates/prisma/src/users/users.controller.ts +21 -21
- package/templates/prisma/src/users/users.service.spec.ts +11 -11
- package/templates/prisma/src/users/users.service.ts +4 -4
- package/templates/prisma/test/auth.e2e-spec.ts +8 -8
- package/templates/prisma/test/session-auth.e2e-spec.ts +9 -9
- package/templates/prisma/test/users.e2e-spec.ts +14 -14
- package/templates/typeorm/src/auth/auth.controller.ts +35 -35
- package/templates/typeorm/src/auth/auth.service.spec.ts +4 -4
- package/templates/typeorm/src/auth/auth.service.ts +9 -9
- package/templates/typeorm/src/auth/dto/forgot-password.dto.ts +2 -2
- package/templates/typeorm/src/auth/dto/login.dto.ts +3 -3
- package/templates/typeorm/src/auth/dto/register.dto.ts +4 -4
- package/templates/typeorm/src/auth/dto/reset-password.dto.ts +3 -3
- package/templates/typeorm/src/auth/guards/github-auth.guard.ts +2 -2
- package/templates/typeorm/src/auth/guards/google-auth.guard.ts +2 -2
- package/templates/typeorm/src/auth/guards/session-auth.guard.spec.ts +4 -4
- package/templates/typeorm/src/auth/guards/session-auth.guard.ts +2 -2
- package/templates/typeorm/src/auth/session.service.spec.ts +5 -5
- package/templates/typeorm/src/auth/session.service.ts +5 -5
- package/templates/typeorm/src/auth/strategies/github.strategy.ts +3 -3
- package/templates/typeorm/src/auth/token.service.ts +3 -3
- package/templates/typeorm/src/common/filters/http-exception.filter.ts +1 -1
- package/templates/typeorm/src/common/guards/permissions.guard.spec.ts +7 -7
- package/templates/typeorm/src/common/guards/roles.guard.spec.ts +4 -4
- package/templates/typeorm/src/common/middleware/csrf.middleware.spec.ts +5 -5
- package/templates/typeorm/src/common/middleware/csrf.middleware.ts +2 -2
- package/templates/typeorm/src/common/utils/avatar-storage.util.ts +2 -2
- package/templates/typeorm/src/config/env.validation.ts +1 -1
- package/templates/typeorm/src/database/data-source.ts +2 -2
- package/templates/typeorm/src/database/seed.ts +3 -3
- package/templates/typeorm/src/database/typeorm-options.ts +2 -2
- package/templates/typeorm/src/health/health.controller.ts +2 -2
- package/templates/typeorm/src/health/indicators/redis-health.indicator.spec.ts +5 -5
- package/templates/typeorm/src/health/indicators/redis-health.indicator.ts +3 -3
- package/templates/typeorm/src/health/indicators/typeorm-health.indicator.spec.ts +4 -4
- package/templates/typeorm/src/health/indicators/typeorm-health.indicator.ts +2 -2
- package/templates/typeorm/src/mail/mail.processor.ts +4 -4
- package/templates/typeorm/src/mail/templates/email-templates.ts +7 -7
- package/templates/typeorm/src/metrics/metrics.service.ts +3 -3
- package/templates/typeorm/src/users/dto/create-user.dto.ts +5 -5
- package/templates/typeorm/src/users/dto/find-users-query.dto.ts +3 -3
- package/templates/typeorm/src/users/users.controller.ts +21 -21
- package/templates/typeorm/src/users/users.service.spec.ts +12 -12
- package/templates/typeorm/src/users/users.service.ts +4 -4
- package/templates/typeorm/test/auth.e2e-spec.ts +8 -8
- package/templates/typeorm/test/session-auth.e2e-spec.ts +9 -9
- package/templates/typeorm/test/users.e2e-spec.ts +18 -18
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ArgumentsHost,
|
|
3
|
-
Catch,
|
|
4
|
-
ExceptionFilter,
|
|
5
|
-
HttpException,
|
|
6
|
-
HttpStatus,
|
|
7
|
-
} from '@nestjs/common';
|
|
8
|
-
import { Request, Response } from 'express';
|
|
9
|
-
|
|
10
|
-
@Catch()
|
|
11
|
-
export class HttpExceptionFilter implements ExceptionFilter {
|
|
12
|
-
catch(exception: unknown, host: ArgumentsHost) {
|
|
13
|
-
const ctx = host.switchToHttp();
|
|
14
|
-
const response = ctx.getResponse<Response>();
|
|
15
|
-
const request = ctx.getRequest<Request>();
|
|
16
|
-
|
|
17
|
-
const status =
|
|
18
|
-
exception instanceof HttpException
|
|
19
|
-
? exception.getStatus()
|
|
20
|
-
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
21
|
-
|
|
22
|
-
const message =
|
|
23
|
-
exception instanceof HttpException
|
|
24
|
-
? exception.getResponse()
|
|
25
|
-
: '
|
|
26
|
-
|
|
27
|
-
response.status(status).json({
|
|
28
|
-
statusCode: status,
|
|
29
|
-
path: request.url,
|
|
30
|
-
timestamp: new Date().toISOString(),
|
|
31
|
-
message,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
ArgumentsHost,
|
|
3
|
+
Catch,
|
|
4
|
+
ExceptionFilter,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { Request, Response } from 'express';
|
|
9
|
+
|
|
10
|
+
@Catch()
|
|
11
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
12
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
13
|
+
const ctx = host.switchToHttp();
|
|
14
|
+
const response = ctx.getResponse<Response>();
|
|
15
|
+
const request = ctx.getRequest<Request>();
|
|
16
|
+
|
|
17
|
+
const status =
|
|
18
|
+
exception instanceof HttpException
|
|
19
|
+
? exception.getStatus()
|
|
20
|
+
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
21
|
+
|
|
22
|
+
const message =
|
|
23
|
+
exception instanceof HttpException
|
|
24
|
+
? exception.getResponse()
|
|
25
|
+
: 'Internal server error';
|
|
26
|
+
|
|
27
|
+
response.status(status).json({
|
|
28
|
+
statusCode: status,
|
|
29
|
+
path: request.url,
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
message,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -15,14 +15,14 @@ function createContext(user?: { role: Role }): ExecutionContext {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
describe('PermissionsGuard', () => {
|
|
18
|
-
it('
|
|
18
|
+
it('allows access when the route requires no permission', () => {
|
|
19
19
|
const reflector = { getAllAndOverride: vi.fn().mockReturnValue(undefined) } as unknown as Reflector;
|
|
20
20
|
const guard = new PermissionsGuard(reflector);
|
|
21
21
|
|
|
22
22
|
expect(guard.canActivate(createContext({ role: Role.USER }))).toBe(true);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it('
|
|
25
|
+
it('allows access when the user role has the required permission', () => {
|
|
26
26
|
const reflector = {
|
|
27
27
|
getAllAndOverride: vi.fn().mockReturnValue([Permission.UserRead]),
|
|
28
28
|
} as unknown as Reflector;
|
|
@@ -31,7 +31,7 @@ describe('PermissionsGuard', () => {
|
|
|
31
31
|
expect(guard.canActivate(createContext({ role: Role.USER }))).toBe(true);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
it('
|
|
34
|
+
it('blocks access when the user role lacks the required permission', () => {
|
|
35
35
|
const reflector = {
|
|
36
36
|
getAllAndOverride: vi.fn().mockReturnValue([Permission.UserDelete]),
|
|
37
37
|
} as unknown as Reflector;
|
|
@@ -40,15 +40,15 @@ describe('PermissionsGuard', () => {
|
|
|
40
40
|
expect(guard.canActivate(createContext({ role: Role.USER }))).toBe(false);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
it('
|
|
43
|
+
it('requires all permissions when more than one is requested', () => {
|
|
44
44
|
const reflector = {
|
|
45
45
|
getAllAndOverride: vi.fn().mockReturnValue([Permission.UserRead, Permission.UserUpdate]),
|
|
46
46
|
} as unknown as Reflector;
|
|
47
47
|
const guard = new PermissionsGuard(reflector);
|
|
48
48
|
|
|
49
|
-
// MANAGER
|
|
49
|
+
// MANAGER has user:read and user:update
|
|
50
50
|
expect(guard.canActivate(createContext({ role: Role.MANAGER }))).toBe(true);
|
|
51
|
-
// USER
|
|
51
|
+
// USER only has user:read
|
|
52
52
|
expect(guard.canActivate(createContext({ role: Role.USER }))).toBe(false);
|
|
53
53
|
});
|
|
54
|
-
});
|
|
54
|
+
});
|
|
@@ -14,24 +14,24 @@ function createContext(user?: { role: Role }): ExecutionContext {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
describe('RolesGuard', () => {
|
|
17
|
-
it('
|
|
17
|
+
it('allows access when the route requires no role', () => {
|
|
18
18
|
const reflector = { getAllAndOverride: vi.fn().mockReturnValue(undefined) } as unknown as Reflector;
|
|
19
19
|
const guard = new RolesGuard(reflector);
|
|
20
20
|
|
|
21
21
|
expect(guard.canActivate(createContext({ role: Role.USER }))).toBe(true);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it('
|
|
24
|
+
it('allows access when the user has the required role', () => {
|
|
25
25
|
const reflector = { getAllAndOverride: vi.fn().mockReturnValue([Role.ADMIN]) } as unknown as Reflector;
|
|
26
26
|
const guard = new RolesGuard(reflector);
|
|
27
27
|
|
|
28
28
|
expect(guard.canActivate(createContext({ role: Role.ADMIN }))).toBe(true);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it('
|
|
31
|
+
it('blocks access when the user does not have the required role', () => {
|
|
32
32
|
const reflector = { getAllAndOverride: vi.fn().mockReturnValue([Role.ADMIN]) } as unknown as Reflector;
|
|
33
33
|
const guard = new RolesGuard(reflector);
|
|
34
34
|
|
|
35
35
|
expect(guard.canActivate(createContext({ role: Role.USER }))).toBe(false);
|
|
36
36
|
});
|
|
37
|
-
});
|
|
37
|
+
});
|
|
@@ -36,7 +36,7 @@ describe('CsrfMiddleware', () => {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
it.each(['GET', 'HEAD', 'OPTIONS'])(
|
|
39
|
-
'
|
|
39
|
+
'allows the safe %s method without a token',
|
|
40
40
|
(method) => {
|
|
41
41
|
const request = createRequest(method);
|
|
42
42
|
|
|
@@ -50,7 +50,7 @@ describe('CsrfMiddleware', () => {
|
|
|
50
50
|
},
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
-
it('
|
|
53
|
+
it('rejects a request without a CSRF token', () => {
|
|
54
54
|
const request = createRequest('POST');
|
|
55
55
|
|
|
56
56
|
expect(() =>
|
|
@@ -64,7 +64,7 @@ describe('CsrfMiddleware', () => {
|
|
|
64
64
|
expect(next).not.toHaveBeenCalled();
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
it('
|
|
67
|
+
it('rejects when the header token differs from the session token', () => {
|
|
68
68
|
const request = createRequest(
|
|
69
69
|
'PATCH',
|
|
70
70
|
'session-token',
|
|
@@ -82,7 +82,7 @@ describe('CsrfMiddleware', () => {
|
|
|
82
82
|
expect(next).not.toHaveBeenCalled();
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
it('
|
|
85
|
+
it('allows the request when the token matches the session value', () => {
|
|
86
86
|
const token = 'a'.repeat(64);
|
|
87
87
|
const request = createRequest('DELETE', token, token);
|
|
88
88
|
|
|
@@ -94,4 +94,4 @@ describe('CsrfMiddleware', () => {
|
|
|
94
94
|
|
|
95
95
|
expect(next).toHaveBeenCalledOnce();
|
|
96
96
|
});
|
|
97
|
-
});
|
|
97
|
+
});
|
|
@@ -27,7 +27,7 @@ export class CsrfMiddleware implements NestMiddleware {
|
|
|
27
27
|
!this.tokensMatch(sessionToken, headerToken)
|
|
28
28
|
) {
|
|
29
29
|
throw new ForbiddenException(
|
|
30
|
-
'
|
|
30
|
+
'Invalid or missing CSRF token',
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -43,4 +43,4 @@ export class CsrfMiddleware implements NestMiddleware {
|
|
|
43
43
|
timingSafeEqual(expectedBuffer, receivedBuffer)
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { BadRequestException } from '@nestjs/common';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
3
|
-
import { diskStorage } from 'multer';
|
|
4
|
-
import { extname } from 'path';
|
|
5
|
-
|
|
6
|
-
const ALLOWED_MIME_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
|
|
7
|
-
|
|
8
|
-
export const avatarStorage = diskStorage({
|
|
9
|
-
destination: './uploads/avatars',
|
|
10
|
-
filename: (_req, file, callback) => {
|
|
11
|
-
const uniqueName = `${randomUUID()}${extname(file.originalname)}`;
|
|
12
|
-
callback(null, uniqueName);
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export function avatarFileFilter(
|
|
17
|
-
_req: unknown,
|
|
18
|
-
file: Express.Multer.File,
|
|
19
|
-
callback: (error: Error | null, acceptFile: boolean) => void,
|
|
20
|
-
) {
|
|
21
|
-
if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
|
|
22
|
-
callback(
|
|
23
|
-
new BadRequestException('
|
|
24
|
-
false,
|
|
25
|
-
);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
callback(null, true);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const AVATAR_MAX_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
|
|
1
|
+
import { BadRequestException } from '@nestjs/common';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { diskStorage } from 'multer';
|
|
4
|
+
import { extname } from 'path';
|
|
5
|
+
|
|
6
|
+
const ALLOWED_MIME_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
|
|
7
|
+
|
|
8
|
+
export const avatarStorage = diskStorage({
|
|
9
|
+
destination: './uploads/avatars',
|
|
10
|
+
filename: (_req, file, callback) => {
|
|
11
|
+
const uniqueName = `${randomUUID()}${extname(file.originalname)}`;
|
|
12
|
+
callback(null, uniqueName);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function avatarFileFilter(
|
|
17
|
+
_req: unknown,
|
|
18
|
+
file: Express.Multer.File,
|
|
19
|
+
callback: (error: Error | null, acceptFile: boolean) => void,
|
|
20
|
+
) {
|
|
21
|
+
if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
|
|
22
|
+
callback(
|
|
23
|
+
new BadRequestException('Unsupported image format (use PNG, JPEG, or WEBP)'),
|
|
24
|
+
false,
|
|
25
|
+
);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
callback(null, true);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const AVATAR_MAX_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
|
|
@@ -42,7 +42,7 @@ export function validateEnv(config: Record<string, unknown>): EnvConfig {
|
|
|
42
42
|
const issues = parsed.error.issues
|
|
43
43
|
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
44
44
|
.join('\n');
|
|
45
|
-
throw new Error(`
|
|
45
|
+
throw new Error(`Invalid environment variables:\n${issues}`);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
return parsed.data;
|
|
@@ -33,7 +33,7 @@ export class HealthController {
|
|
|
33
33
|
@Get()
|
|
34
34
|
@HealthCheck()
|
|
35
35
|
// nestforge:feature:swagger
|
|
36
|
-
@ApiOperation({ summary: '
|
|
36
|
+
@ApiOperation({ summary: 'Checks API health (database, memory, disk, and other configured dependencies)' })
|
|
37
37
|
// nestforge:feature:swagger:end
|
|
38
38
|
check() {
|
|
39
39
|
return this.health.check([
|
|
@@ -46,4 +46,4 @@ export class HealthController {
|
|
|
46
46
|
() => this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),
|
|
47
47
|
]);
|
|
48
48
|
}
|
|
49
|
-
}
|
|
49
|
+
}
|
|
@@ -4,7 +4,7 @@ import { PrismaHealthIndicator } from './prisma-health.indicator';
|
|
|
4
4
|
import { PrismaService } from '../../database/prisma.service';
|
|
5
5
|
|
|
6
6
|
describe('PrismaHealthIndicator', () => {
|
|
7
|
-
it('
|
|
7
|
+
it('returns an up status when the query succeeds', async () => {
|
|
8
8
|
const prisma = {
|
|
9
9
|
// nestforge:feature:database:relational
|
|
10
10
|
$queryRaw: vi.fn().mockResolvedValue([{ '?column?': 1 }]),
|
|
@@ -23,13 +23,13 @@ describe('PrismaHealthIndicator', () => {
|
|
|
23
23
|
// nestforge:feature:database:mongodb:end
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
it('
|
|
26
|
+
it('throws HealthCheckError when the query fails', async () => {
|
|
27
27
|
const prisma = {
|
|
28
28
|
// nestforge:feature:database:relational
|
|
29
|
-
$queryRaw: vi.fn().mockRejectedValue(new Error('
|
|
29
|
+
$queryRaw: vi.fn().mockRejectedValue(new Error('connection refused')),
|
|
30
30
|
// nestforge:feature:database:relational:end
|
|
31
31
|
// nestforge:feature:database:mongodb
|
|
32
|
-
$runCommandRaw: vi.fn().mockRejectedValue(new Error('
|
|
32
|
+
$runCommandRaw: vi.fn().mockRejectedValue(new Error('connection refused')),
|
|
33
33
|
// nestforge:feature:database:mongodb:end
|
|
34
34
|
};
|
|
35
35
|
const indicator = new PrismaHealthIndicator(prisma as unknown as PrismaService);
|
|
@@ -16,7 +16,7 @@ export class PrismaHealthIndicator {
|
|
|
16
16
|
// nestforge:feature:database:mongodb:end
|
|
17
17
|
return { [key]: { status: 'up' } };
|
|
18
18
|
} catch (error) {
|
|
19
|
-
throw new HealthCheckError('
|
|
19
|
+
throw new HealthCheckError('Database unavailable', {
|
|
20
20
|
[key]: { status: 'down', message: (error as Error).message },
|
|
21
21
|
});
|
|
22
22
|
}
|
|
@@ -14,7 +14,7 @@ vi.mock('ioredis', () => ({
|
|
|
14
14
|
})),
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// Must be imported after vi.mock; otherwise it loads the real ioredis package.
|
|
18
18
|
const { RedisHealthIndicator } = await import('./redis-health.indicator');
|
|
19
19
|
|
|
20
20
|
describe('RedisHealthIndicator', () => {
|
|
@@ -24,7 +24,7 @@ describe('RedisHealthIndicator', () => {
|
|
|
24
24
|
disconnectMock.mockReset();
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it('
|
|
27
|
+
it('returns an up status when PING returns PONG', async () => {
|
|
28
28
|
connectMock.mockResolvedValue(undefined);
|
|
29
29
|
pingMock.mockResolvedValue('PONG');
|
|
30
30
|
|
|
@@ -35,7 +35,7 @@ describe('RedisHealthIndicator', () => {
|
|
|
35
35
|
expect(disconnectMock).toHaveBeenCalled();
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
it('
|
|
38
|
+
it('throws HealthCheckError when PING does not return PONG', async () => {
|
|
39
39
|
connectMock.mockResolvedValue(undefined);
|
|
40
40
|
pingMock.mockResolvedValue('ALGO_INESPERADO');
|
|
41
41
|
|
|
@@ -44,11 +44,11 @@ describe('RedisHealthIndicator', () => {
|
|
|
44
44
|
await expect(indicator.isHealthy('redis')).rejects.toBeInstanceOf(HealthCheckError);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
it('
|
|
47
|
+
it('throws HealthCheckError when the connection fails', async () => {
|
|
48
48
|
connectMock.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
49
49
|
|
|
50
50
|
const indicator = new RedisHealthIndicator();
|
|
51
51
|
|
|
52
52
|
await expect(indicator.isHealthy('redis')).rejects.toBeInstanceOf(HealthCheckError);
|
|
53
53
|
});
|
|
54
|
-
});
|
|
54
|
+
});
|
|
@@ -18,16 +18,16 @@ export class RedisHealthIndicator {
|
|
|
18
18
|
const response = await client.ping();
|
|
19
19
|
|
|
20
20
|
if (response !== 'PONG') {
|
|
21
|
-
throw new Error('
|
|
21
|
+
throw new Error('Unexpected Redis response');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
return { [key]: { status: 'up' } };
|
|
25
25
|
} catch (error) {
|
|
26
|
-
throw new HealthCheckError('Redis
|
|
26
|
+
throw new HealthCheckError('Redis unavailable', {
|
|
27
27
|
[key]: { status: 'down', message: (error as Error).message },
|
|
28
28
|
});
|
|
29
29
|
} finally {
|
|
30
30
|
client.disconnect();
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
|
@@ -27,13 +27,13 @@ export class MailProcessor extends WorkerHost {
|
|
|
27
27
|
|
|
28
28
|
switch (job.name) {
|
|
29
29
|
case MailJob.VerifyEmail:
|
|
30
|
-
await this.send(to, '
|
|
30
|
+
await this.send(to, 'Confirm your email', verifyEmailTemplate(name, token));
|
|
31
31
|
break;
|
|
32
32
|
case MailJob.ResetPassword:
|
|
33
|
-
await this.send(to, '
|
|
33
|
+
await this.send(to, 'Password reset', resetPasswordTemplate(name, token));
|
|
34
34
|
break;
|
|
35
35
|
default:
|
|
36
|
-
this.logger.warn(`
|
|
36
|
+
this.logger.warn(`Unknown email job: ${job.name}`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -45,4 +45,4 @@ export class MailProcessor extends WorkerHost {
|
|
|
45
45
|
html,
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|
|
@@ -6,10 +6,10 @@ export function verifyEmailTemplate(name: string, token: string): string {
|
|
|
6
6
|
|
|
7
7
|
return `
|
|
8
8
|
<div style="font-family: sans-serif; line-height: 1.5;">
|
|
9
|
-
<h2>
|
|
10
|
-
<p>
|
|
9
|
+
<h2>Hello, ${name}!</h2>
|
|
10
|
+
<p>Confirm your email by clicking the link below:</p>
|
|
11
11
|
<p><a href="${link}">${link}</a></p>
|
|
12
|
-
<p>
|
|
12
|
+
<p>This link expires in 24 hours.</p>
|
|
13
13
|
</div>
|
|
14
14
|
`;
|
|
15
15
|
}
|
|
@@ -19,10 +19,10 @@ export function resetPasswordTemplate(name: string, token: string): string {
|
|
|
19
19
|
|
|
20
20
|
return `
|
|
21
21
|
<div style="font-family: sans-serif; line-height: 1.5;">
|
|
22
|
-
<h2>
|
|
23
|
-
<p>
|
|
22
|
+
<h2>Hello, ${name}!</h2>
|
|
23
|
+
<p>We received a request to reset your password. Click the link below to continue:</p>
|
|
24
24
|
<p><a href="${link}">${link}</a></p>
|
|
25
|
-
<p>
|
|
25
|
+
<p>If you did not request this, you can ignore this email. The link expires in 1 hour.</p>
|
|
26
26
|
</div>
|
|
27
27
|
`;
|
|
28
|
-
}
|
|
28
|
+
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import * as client from 'prom-client';
|
|
3
|
-
|
|
4
|
-
@Injectable()
|
|
5
|
-
export class MetricsService {
|
|
6
|
-
readonly registry = new client.Registry();
|
|
7
|
-
|
|
8
|
-
readonly httpRequestDuration = new client.Histogram({
|
|
9
|
-
name: 'http_request_duration_seconds',
|
|
10
|
-
help: '
|
|
11
|
-
labelNames: ['method', 'route', 'status_code'],
|
|
12
|
-
buckets: [0.05, 0.1, 0.3, 0.5, 1, 2, 5],
|
|
13
|
-
registers: [this.registry],
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
readonly httpRequestsTotal = new client.Counter({
|
|
17
|
-
name: 'http_requests_total',
|
|
18
|
-
help: 'Total
|
|
19
|
-
labelNames: ['method', 'route', 'status_code'],
|
|
20
|
-
registers: [this.registry],
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
constructor() {
|
|
24
|
-
client.collectDefaultMetrics({ register: this.registry });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async getMetrics(): Promise<string> {
|
|
28
|
-
return this.registry.metrics();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
getContentType(): string {
|
|
32
|
-
return this.registry.contentType;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import * as client from 'prom-client';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class MetricsService {
|
|
6
|
+
readonly registry = new client.Registry();
|
|
7
|
+
|
|
8
|
+
readonly httpRequestDuration = new client.Histogram({
|
|
9
|
+
name: 'http_request_duration_seconds',
|
|
10
|
+
help: 'HTTP request duration in seconds',
|
|
11
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
12
|
+
buckets: [0.05, 0.1, 0.3, 0.5, 1, 2, 5],
|
|
13
|
+
registers: [this.registry],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
readonly httpRequestsTotal = new client.Counter({
|
|
17
|
+
name: 'http_requests_total',
|
|
18
|
+
help: 'Total number of HTTP requests received',
|
|
19
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
20
|
+
registers: [this.registry],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
client.collectDefaultMetrics({ register: this.registry });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getMetrics(): Promise<string> {
|
|
28
|
+
return this.registry.metrics();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getContentType(): string {
|
|
32
|
+
return this.registry.contentType;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { createZodDto } from 'nestjs-zod';
|
|
3
|
-
import { Role } from '@prisma/client';
|
|
4
|
-
|
|
5
|
-
export const createUserSchema = z.object({
|
|
6
|
-
name: z.string().min(2).describe('
|
|
7
|
-
email: z.string().email().describe('
|
|
8
|
-
password: z.string().min(8).describe('
|
|
9
|
-
role: z.nativeEnum(Role).optional().describe('
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export class CreateUserDto extends createZodDto(createUserSchema) { }
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createZodDto } from 'nestjs-zod';
|
|
3
|
+
import { Role } from '@prisma/client';
|
|
4
|
+
|
|
5
|
+
export const createUserSchema = z.object({
|
|
6
|
+
name: z.string().min(2).describe('Full name'),
|
|
7
|
+
email: z.string().email().describe('User email'),
|
|
8
|
+
password: z.string().min(8).describe('Password (minimum 8 characters)'),
|
|
9
|
+
role: z.nativeEnum(Role).optional().describe('User role (default: USER)'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export class CreateUserDto extends createZodDto(createUserSchema) { }
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { createZodDto } from 'nestjs-zod';
|
|
3
|
-
import { Role } from '@prisma/client';
|
|
4
|
-
|
|
5
|
-
export const findUsersQuerySchema = z.object({
|
|
6
|
-
page: z.coerce.number().int().min(1).default(1).describe('
|
|
7
|
-
limit: z.coerce.number().int().min(1).max(100).default(10).describe('
|
|
8
|
-
search: z.string().optional().describe('Busca por nome ou e-mail'),
|
|
9
|
-
role: z.nativeEnum(Role).optional().describe('Filtra por role'),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export class FindUsersQueryDto extends createZodDto(findUsersQuerySchema) { }
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createZodDto } from 'nestjs-zod';
|
|
3
|
+
import { Role } from '@prisma/client';
|
|
4
|
+
|
|
5
|
+
export const findUsersQuerySchema = z.object({
|
|
6
|
+
page: z.coerce.number().int().min(1).default(1).describe('Current page'),
|
|
7
|
+
limit: z.coerce.number().int().min(1).max(100).default(10).describe('Items per page (maximum 100)'),
|
|
8
|
+
search: z.string().optional().describe('Busca por nome ou e-mail'),
|
|
9
|
+
role: z.nativeEnum(Role).optional().describe('Filtra por role'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export class FindUsersQueryDto extends createZodDto(findUsersQuerySchema) { }
|