nestforge-generator 0.2.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 +24 -7
- package/README.pt-BR.md +24 -7
- package/dist/features/auth-strategy.js +7 -7
- package/dist/features/database.js +35 -0
- package/dist/features/database.js.map +1 -1
- 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 +14 -2
- package/dist/generator.js.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/project-name.js +50 -0
- package/dist/project-name.js.map +1 -0
- package/dist/prompts.js +15 -11
- package/dist/prompts.js.map +1 -1
- package/package.json +2 -1
- package/templates/drizzle/README.md +2 -0
- package/templates/drizzle/README.pt-BR.md +2 -0
- 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/.github/workflows/ci.yml +24 -1
- package/templates/prisma/ARCHITECTURE.md +7 -1
- package/templates/prisma/ARCHITECTURE.pt-BR.md +7 -1
- package/templates/prisma/README.md +11 -7
- package/templates/prisma/README.pt-BR.md +11 -7
- package/templates/prisma/ROADMAP.md +5 -1
- package/templates/prisma/ROADMAP.pt-BR.md +5 -1
- package/templates/prisma/TESTING.md +25 -3
- package/templates/prisma/TESTING.pt-BR.md +25 -3
- package/templates/prisma/docker-compose.yml +29 -1
- package/templates/prisma/docs/adding-a-module.md +246 -230
- package/templates/prisma/docs/features-markers.md +11 -1
- package/templates/prisma/prisma/schema.prisma +4 -0
- 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 +14 -5
- 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 +39 -22
- package/templates/prisma/src/health/indicators/prisma-health.indicator.ts +24 -19
- 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/README.md +2 -0
- package/templates/typeorm/README.pt-BR.md +2 -0
- 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
|
@@ -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
|
+
}
|
|
@@ -20,7 +20,7 @@ export function avatarFileFilter(
|
|
|
20
20
|
) {
|
|
21
21
|
if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
|
|
22
22
|
callback(
|
|
23
|
-
new BadRequestException('
|
|
23
|
+
new BadRequestException('Unsupported image format (use PNG, JPEG, or WEBP)'),
|
|
24
24
|
false,
|
|
25
25
|
);
|
|
26
26
|
return;
|
|
@@ -29,4 +29,4 @@ export function avatarFileFilter(
|
|
|
29
29
|
callback(null, true);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export const AVATAR_MAX_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
|
|
32
|
+
export const AVATAR_MAX_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
|
|
@@ -43,7 +43,7 @@ export function validateEnv(config: Record<string, unknown>): EnvConfig {
|
|
|
43
43
|
const issues = parsed.error.issues
|
|
44
44
|
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
45
45
|
.join('\n');
|
|
46
|
-
throw new Error(`
|
|
46
|
+
throw new Error(`Invalid environment variables:\n${issues}`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
return parsed.data;
|
|
@@ -46,7 +46,7 @@ const SEED_USERS: SeedUser[] = [
|
|
|
46
46
|
role: Role.MANAGER,
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
|
-
name: '
|
|
49
|
+
name: 'User',
|
|
50
50
|
email: 'user@nestforge.dev',
|
|
51
51
|
password: 'user1234',
|
|
52
52
|
role: Role.USER,
|
|
@@ -57,7 +57,7 @@ const databaseUrl = process.env.DATABASE_URL;
|
|
|
57
57
|
|
|
58
58
|
if (!databaseUrl) {
|
|
59
59
|
throw new Error(
|
|
60
|
-
'DATABASE_URL
|
|
60
|
+
'DATABASE_URL is not defined',
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -144,9 +144,9 @@ async function seed(): Promise<void> {
|
|
|
144
144
|
|
|
145
145
|
seed().catch((error: unknown) => {
|
|
146
146
|
console.error(
|
|
147
|
-
'
|
|
147
|
+
'Error while running the seed:',
|
|
148
148
|
error,
|
|
149
149
|
);
|
|
150
150
|
|
|
151
151
|
process.exitCode = 1;
|
|
152
|
-
});
|
|
152
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -9,7 +9,7 @@ import type { DatabaseClient } from '../../database/database.types';
|
|
|
9
9
|
import { DrizzleHealthIndicator } from './drizzle-health.indicator';
|
|
10
10
|
|
|
11
11
|
describe('DrizzleHealthIndicator', () => {
|
|
12
|
-
it('
|
|
12
|
+
it('returns an up status when the database query succeeds', async () => {
|
|
13
13
|
const get = vi.fn().mockReturnValue({
|
|
14
14
|
result: 1,
|
|
15
15
|
});
|
|
@@ -41,16 +41,16 @@ describe('DrizzleHealthIndicator', () => {
|
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it('
|
|
44
|
+
it('throws HealthCheckError when the database query fails', async () => {
|
|
45
45
|
const get = vi.fn(() => {
|
|
46
|
-
throw new Error('
|
|
46
|
+
throw new Error('connection refused');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const databaseClient = {
|
|
50
50
|
query: vi
|
|
51
51
|
.fn()
|
|
52
52
|
.mockRejectedValue(
|
|
53
|
-
new Error('
|
|
53
|
+
new Error('connection refused'),
|
|
54
54
|
),
|
|
55
55
|
prepare: vi.fn(() => ({
|
|
56
56
|
get,
|
|
@@ -68,4 +68,4 @@ describe('DrizzleHealthIndicator', () => {
|
|
|
68
68
|
HealthCheckError,
|
|
69
69
|
);
|
|
70
70
|
});
|
|
71
|
-
});
|
|
71
|
+
});
|
|
@@ -39,7 +39,7 @@ export class DrizzleHealthIndicator {
|
|
|
39
39
|
};
|
|
40
40
|
} catch (error) {
|
|
41
41
|
throw new HealthCheckError(
|
|
42
|
-
'
|
|
42
|
+
'Database unavailable',
|
|
43
43
|
{
|
|
44
44
|
[key]: {
|
|
45
45
|
status: 'down',
|
|
@@ -49,4 +49,4 @@ export class DrizzleHealthIndicator {
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -7,7 +7,7 @@ export class MetricsService {
|
|
|
7
7
|
|
|
8
8
|
readonly httpRequestDuration = new client.Histogram({
|
|
9
9
|
name: 'http_request_duration_seconds',
|
|
10
|
-
help: '
|
|
10
|
+
help: 'HTTP request duration in seconds',
|
|
11
11
|
labelNames: ['method', 'route', 'status_code'],
|
|
12
12
|
buckets: [0.05, 0.1, 0.3, 0.5, 1, 2, 5],
|
|
13
13
|
registers: [this.registry],
|
|
@@ -15,7 +15,7 @@ export class MetricsService {
|
|
|
15
15
|
|
|
16
16
|
readonly httpRequestsTotal = new client.Counter({
|
|
17
17
|
name: 'http_requests_total',
|
|
18
|
-
help: 'Total
|
|
18
|
+
help: 'Total number of HTTP requests received',
|
|
19
19
|
labelNames: ['method', 'route', 'status_code'],
|
|
20
20
|
registers: [this.registry],
|
|
21
21
|
});
|
|
@@ -31,4 +31,4 @@ export class MetricsService {
|
|
|
31
31
|
getContentType(): string {
|
|
32
32
|
return this.registry.contentType;
|
|
33
33
|
}
|
|
34
|
-
}
|
|
34
|
+
}
|
|
@@ -3,10 +3,10 @@ import { createZodDto } from 'nestjs-zod';
|
|
|
3
3
|
import { Role } from '../../common/constants/role.enum';
|
|
4
4
|
|
|
5
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('
|
|
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
10
|
});
|
|
11
11
|
|
|
12
|
-
export class CreateUserDto extends createZodDto(createUserSchema) { }
|
|
12
|
+
export class CreateUserDto extends createZodDto(createUserSchema) { }
|
|
@@ -3,10 +3,10 @@ import { createZodDto } from 'nestjs-zod';
|
|
|
3
3
|
import { Role } from '../../common/constants/role.enum';
|
|
4
4
|
|
|
5
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('
|
|
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
8
|
search: z.string().optional().describe('Busca por nome ou e-mail'),
|
|
9
9
|
role: z.nativeEnum(Role).optional().describe('Filtra por role'),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
export class FindUsersQueryDto extends createZodDto(findUsersQuerySchema) { }
|
|
12
|
+
export class FindUsersQueryDto extends createZodDto(findUsersQuerySchema) { }
|
|
@@ -57,9 +57,9 @@ export class UsersController {
|
|
|
57
57
|
@Permissions(Permission.UserCreate)
|
|
58
58
|
// nestforge:feature:rbac:end
|
|
59
59
|
// nestforge:feature:swagger
|
|
60
|
-
@ApiOperation({ summary: '
|
|
61
|
-
@ApiResponse({ status: 201, description: '
|
|
62
|
-
@ApiResponse({ status: 403, description: '
|
|
60
|
+
@ApiOperation({ summary: 'Creates a user (requires the user:create permission)' })
|
|
61
|
+
@ApiResponse({ status: 201, description: 'User created', schema: { example: USER_EXAMPLE } })
|
|
62
|
+
@ApiResponse({ status: 403, description: 'Missing the user:create permission', schema: { example: FORBIDDEN_EXAMPLE } })
|
|
63
63
|
// nestforge:feature:swagger:end
|
|
64
64
|
create(@Body() dto: CreateUserDto) {
|
|
65
65
|
return this.usersService.create(dto);
|
|
@@ -70,10 +70,10 @@ export class UsersController {
|
|
|
70
70
|
@Permissions(Permission.UserRead)
|
|
71
71
|
// nestforge:feature:rbac:end
|
|
72
72
|
// nestforge:feature:swagger
|
|
73
|
-
@ApiOperation({ summary: '
|
|
73
|
+
@ApiOperation({ summary: 'Lists users with pagination and filters (requires the user:read permission)' })
|
|
74
74
|
@ApiResponse({
|
|
75
75
|
status: 200,
|
|
76
|
-
description: '
|
|
76
|
+
description: 'Paginated list',
|
|
77
77
|
schema: {
|
|
78
78
|
example: {
|
|
79
79
|
data: [USER_EXAMPLE],
|
|
@@ -88,8 +88,8 @@ export class UsersController {
|
|
|
88
88
|
|
|
89
89
|
@Get('me')
|
|
90
90
|
// nestforge:feature:swagger
|
|
91
|
-
@ApiOperation({ summary: '
|
|
92
|
-
@ApiResponse({ status: 200, description: '
|
|
91
|
+
@ApiOperation({ summary: 'Returns the authenticated user' })
|
|
92
|
+
@ApiResponse({ status: 200, description: 'Authenticated user', schema: { example: USER_EXAMPLE } })
|
|
93
93
|
// nestforge:feature:swagger:end
|
|
94
94
|
findMe(@CurrentUser() user: { id: string }) {
|
|
95
95
|
return this.usersService.findOne(user.id);
|
|
@@ -97,17 +97,17 @@ export class UsersController {
|
|
|
97
97
|
|
|
98
98
|
@Post('me/avatar')
|
|
99
99
|
// nestforge:feature:swagger
|
|
100
|
-
@ApiOperation({ summary: '
|
|
100
|
+
@ApiOperation({ summary: 'Uploads an avatar for the authenticated user' })
|
|
101
101
|
@ApiConsumes('multipart/form-data')
|
|
102
102
|
@ApiResponse({
|
|
103
103
|
status: 201,
|
|
104
|
-
description: 'Avatar
|
|
104
|
+
description: 'Avatar updated',
|
|
105
105
|
schema: { example: { ...USER_EXAMPLE, avatarUrl: '/uploads/avatars/f47ac10b.png' } },
|
|
106
106
|
})
|
|
107
107
|
@ApiResponse({
|
|
108
108
|
status: 400,
|
|
109
|
-
description: '
|
|
110
|
-
schema: { example: { statusCode: 400, message: '
|
|
109
|
+
description: 'Unsupported image format or file larger than 2 MB',
|
|
110
|
+
schema: { example: { statusCode: 400, message: 'Unsupported image format (use PNG, JPEG, or WEBP)' } },
|
|
111
111
|
})
|
|
112
112
|
// nestforge:feature:swagger:end
|
|
113
113
|
@UseInterceptors(
|
|
@@ -129,9 +129,9 @@ export class UsersController {
|
|
|
129
129
|
@Permissions(Permission.UserRead)
|
|
130
130
|
// nestforge:feature:rbac:end
|
|
131
131
|
// nestforge:feature:swagger
|
|
132
|
-
@ApiOperation({ summary: '
|
|
133
|
-
@ApiResponse({ status: 200, description: '
|
|
134
|
-
@ApiResponse({ status: 404, description: '
|
|
132
|
+
@ApiOperation({ summary: 'Finds a user by ID (requires the user:read permission)' })
|
|
133
|
+
@ApiResponse({ status: 200, description: 'User found', schema: { example: USER_EXAMPLE } })
|
|
134
|
+
@ApiResponse({ status: 404, description: 'User not found', schema: { example: { statusCode: 404, message: 'User not found' } } })
|
|
135
135
|
// nestforge:feature:swagger:end
|
|
136
136
|
findOne(@Param('id') id: string) {
|
|
137
137
|
return this.usersService.findOne(id);
|
|
@@ -142,9 +142,9 @@ export class UsersController {
|
|
|
142
142
|
@Permissions(Permission.UserUpdate)
|
|
143
143
|
// nestforge:feature:rbac:end
|
|
144
144
|
// nestforge:feature:swagger
|
|
145
|
-
@ApiOperation({ summary: '
|
|
146
|
-
@ApiResponse({ status: 200, description: '
|
|
147
|
-
@ApiResponse({ status: 403, description: '
|
|
145
|
+
@ApiOperation({ summary: 'Updates a user (requires the user:update permission)' })
|
|
146
|
+
@ApiResponse({ status: 200, description: 'User updated', schema: { example: USER_EXAMPLE } })
|
|
147
|
+
@ApiResponse({ status: 403, description: 'Missing the user:update permission', schema: { example: FORBIDDEN_EXAMPLE } })
|
|
148
148
|
// nestforge:feature:swagger:end
|
|
149
149
|
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
|
|
150
150
|
return this.usersService.update(id, dto);
|
|
@@ -155,11 +155,11 @@ export class UsersController {
|
|
|
155
155
|
@Permissions(Permission.UserDelete)
|
|
156
156
|
// nestforge:feature:rbac:end
|
|
157
157
|
// nestforge:feature:swagger
|
|
158
|
-
@ApiOperation({ summary: '
|
|
159
|
-
@ApiResponse({ status: 200, description: '
|
|
160
|
-
@ApiResponse({ status: 403, description: '
|
|
158
|
+
@ApiOperation({ summary: 'Deletes a user (requires the user:delete permission)' })
|
|
159
|
+
@ApiResponse({ status: 200, description: 'User deleted', schema: { example: { message: 'User deleted successfully' } } })
|
|
160
|
+
@ApiResponse({ status: 403, description: 'Missing the user:delete permission', schema: { example: FORBIDDEN_EXAMPLE } })
|
|
161
161
|
// nestforge:feature:swagger:end
|
|
162
162
|
remove(@Param('id') id: string) {
|
|
163
163
|
return this.usersService.remove(id);
|
|
164
164
|
}
|
|
165
|
-
}
|
|
165
|
+
}
|
|
@@ -81,7 +81,7 @@ describe('UsersService', () => {
|
|
|
81
81
|
);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
it('
|
|
84
|
+
it('throws ConflictException when the email already exists during creation', async () => {
|
|
85
85
|
database.select.mockReturnValueOnce(
|
|
86
86
|
createSelectBuilder([
|
|
87
87
|
{
|
|
@@ -94,14 +94,14 @@ describe('UsersService', () => {
|
|
|
94
94
|
usersService.create({
|
|
95
95
|
name: 'Jeiel',
|
|
96
96
|
email: 'jeiel@example.com',
|
|
97
|
-
password: '
|
|
97
|
+
password: 'strongPassword123',
|
|
98
98
|
}),
|
|
99
99
|
).rejects.toBeInstanceOf(ConflictException);
|
|
100
100
|
|
|
101
101
|
expect(database.insert).not.toHaveBeenCalled();
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
it('
|
|
104
|
+
it('creates a user and hides passwordHash during serialization', async () => {
|
|
105
105
|
database.select
|
|
106
106
|
.mockReturnValueOnce(createSelectBuilder([]))
|
|
107
107
|
.mockReturnValueOnce(
|
|
@@ -111,7 +111,7 @@ describe('UsersService', () => {
|
|
|
111
111
|
const result = await usersService.create({
|
|
112
112
|
name: 'Jeiel',
|
|
113
113
|
email: 'jeiel@example.com',
|
|
114
|
-
password: '
|
|
114
|
+
password: 'strongPassword123',
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
expect(result.email).toBe(
|
|
@@ -133,7 +133,7 @@ describe('UsersService', () => {
|
|
|
133
133
|
);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
it('
|
|
136
|
+
it('throws NotFoundException when the user does not exist', async () => {
|
|
137
137
|
database.select.mockReturnValueOnce(
|
|
138
138
|
createSelectBuilder([]),
|
|
139
139
|
);
|
|
@@ -143,7 +143,7 @@ describe('UsersService', () => {
|
|
|
143
143
|
).rejects.toBeInstanceOf(NotFoundException);
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
it('
|
|
146
|
+
it('returns paginated data from findAll', async () => {
|
|
147
147
|
const usersBuilder = createSelectBuilder([
|
|
148
148
|
mockUser,
|
|
149
149
|
]);
|
|
@@ -179,7 +179,7 @@ describe('UsersService', () => {
|
|
|
179
179
|
);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
-
it('
|
|
182
|
+
it('applies search and role filters in findAll', async () => {
|
|
183
183
|
const usersBuilder = createSelectBuilder([
|
|
184
184
|
mockUser,
|
|
185
185
|
]);
|
|
@@ -214,7 +214,7 @@ describe('UsersService', () => {
|
|
|
214
214
|
);
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
it('
|
|
217
|
+
it('updates an existing user', async () => {
|
|
218
218
|
const updatedUser = new UserEntity({
|
|
219
219
|
...mockUser,
|
|
220
220
|
name: 'Novo Nome',
|
|
@@ -248,7 +248,7 @@ describe('UsersService', () => {
|
|
|
248
248
|
expect(updateWhere).toHaveBeenCalled();
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
it('
|
|
251
|
+
it('deletes an existing user', async () => {
|
|
252
252
|
database.select.mockReturnValueOnce(
|
|
253
253
|
createSelectBuilder([mockUser]),
|
|
254
254
|
);
|
|
@@ -261,11 +261,11 @@ describe('UsersService', () => {
|
|
|
261
261
|
expect(deleteWhere).toHaveBeenCalled();
|
|
262
262
|
|
|
263
263
|
expect(result).toEqual({
|
|
264
|
-
message: '
|
|
264
|
+
message: 'User deleted successfully',
|
|
265
265
|
});
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
-
it('
|
|
268
|
+
it('updates a user avatar', async () => {
|
|
269
269
|
const updatedUser = new UserEntity({
|
|
270
270
|
...mockUser,
|
|
271
271
|
avatarUrl: '/uploads/avatars/x.png',
|
|
@@ -298,4 +298,4 @@ describe('UsersService', () => {
|
|
|
298
298
|
|
|
299
299
|
expect(updateWhere).toHaveBeenCalled();
|
|
300
300
|
});
|
|
301
|
-
});
|
|
301
|
+
});
|