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
|
@@ -60,7 +60,7 @@ export class AuthService {
|
|
|
60
60
|
|
|
61
61
|
if (existing) {
|
|
62
62
|
throw new ConflictException(
|
|
63
|
-
'
|
|
63
|
+
'Email already registered',
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -103,7 +103,7 @@ export class AuthService {
|
|
|
103
103
|
|
|
104
104
|
if (!user || !user.passwordHash) {
|
|
105
105
|
throw new UnauthorizedException(
|
|
106
|
-
'
|
|
106
|
+
'Invalid credentials',
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -114,7 +114,7 @@ export class AuthService {
|
|
|
114
114
|
|
|
115
115
|
if (!passwordMatches) {
|
|
116
116
|
throw new UnauthorizedException(
|
|
117
|
-
'
|
|
117
|
+
'Invalid credentials',
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -206,7 +206,7 @@ export class AuthService {
|
|
|
206
206
|
|
|
207
207
|
const genericResponse = {
|
|
208
208
|
message:
|
|
209
|
-
'
|
|
209
|
+
'If the email exists, password reset instructions will be sent',
|
|
210
210
|
};
|
|
211
211
|
|
|
212
212
|
if (!user) {
|
|
@@ -264,7 +264,7 @@ export class AuthService {
|
|
|
264
264
|
stored.expiresAt < new Date()
|
|
265
265
|
) {
|
|
266
266
|
throw new UnauthorizedException(
|
|
267
|
-
'
|
|
267
|
+
'Invalid or expired password reset token',
|
|
268
268
|
);
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -433,7 +433,7 @@ export class AuthService {
|
|
|
433
433
|
// nestforge:feature:database:sqlite:end
|
|
434
434
|
|
|
435
435
|
return {
|
|
436
|
-
message: '
|
|
436
|
+
message: 'Password reset successfully',
|
|
437
437
|
};
|
|
438
438
|
}
|
|
439
439
|
|
|
@@ -457,7 +457,7 @@ export class AuthService {
|
|
|
457
457
|
stored.expiresAt < new Date()
|
|
458
458
|
) {
|
|
459
459
|
throw new UnauthorizedException(
|
|
460
|
-
'
|
|
460
|
+
'Invalid or expired verification token',
|
|
461
461
|
);
|
|
462
462
|
}
|
|
463
463
|
|
|
@@ -563,7 +563,7 @@ export class AuthService {
|
|
|
563
563
|
// nestforge:feature:database:sqlite:end
|
|
564
564
|
|
|
565
565
|
return {
|
|
566
|
-
message: '
|
|
566
|
+
message: 'Email verified successfully',
|
|
567
567
|
};
|
|
568
568
|
}
|
|
569
569
|
|
|
@@ -628,4 +628,4 @@ export class AuthService {
|
|
|
628
628
|
.update(token)
|
|
629
629
|
.digest('hex');
|
|
630
630
|
}
|
|
631
|
-
}
|
|
631
|
+
}
|
|
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { createZodDto } from 'nestjs-zod';
|
|
4
4
|
|
|
5
5
|
export const forgotPasswordSchema = z.object({
|
|
6
|
-
email: z.string().email().describe('
|
|
6
|
+
email: z.string().email().describe('Email of the account to recover'),
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
export class ForgotPasswordDto extends createZodDto(forgotPasswordSchema) { }
|
|
9
|
+
export class ForgotPasswordDto extends createZodDto(forgotPasswordSchema) { }
|
|
@@ -3,8 +3,8 @@ import { z } from 'zod';
|
|
|
3
3
|
import { createZodDto } from 'nestjs-zod';
|
|
4
4
|
|
|
5
5
|
export const loginSchema = z.object({
|
|
6
|
-
email: z.string().email().describe('
|
|
7
|
-
password: z.string().min(8).describe('
|
|
6
|
+
email: z.string().email().describe('User email'),
|
|
7
|
+
password: z.string().min(8).describe('Password'),
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
export class LoginDto extends createZodDto(loginSchema) { }
|
|
10
|
+
export class LoginDto extends createZodDto(loginSchema) { }
|
|
@@ -3,9 +3,9 @@ import { z } from 'zod';
|
|
|
3
3
|
import { createZodDto } from 'nestjs-zod';
|
|
4
4
|
|
|
5
5
|
export const registerSchema = z.object({
|
|
6
|
-
name: z.string().min(2).describe('
|
|
7
|
-
email: z.string().email().describe('
|
|
8
|
-
password: z.string().min(8).describe('
|
|
6
|
+
name: z.string().min(2).describe('User full name'),
|
|
7
|
+
email: z.string().email().describe('User email'),
|
|
8
|
+
password: z.string().min(8).describe('Password (minimum 8 characters)'),
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
export class RegisterDto extends createZodDto(registerSchema) { }
|
|
11
|
+
export class RegisterDto extends createZodDto(registerSchema) { }
|
|
@@ -3,8 +3,8 @@ import { z } from 'zod';
|
|
|
3
3
|
import { createZodDto } from 'nestjs-zod';
|
|
4
4
|
|
|
5
5
|
export const resetPasswordSchema = z.object({
|
|
6
|
-
token: z.string().describe('Token
|
|
7
|
-
password: z.string().min(8).describe('
|
|
6
|
+
token: z.string().describe('Token received by email'),
|
|
7
|
+
password: z.string().min(8).describe('New password (minimum 8 characters)'),
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
export class ResetPasswordDto extends createZodDto(resetPasswordSchema) { }
|
|
10
|
+
export class ResetPasswordDto extends createZodDto(resetPasswordSchema) { }
|
|
@@ -4,7 +4,7 @@ import { AuthGuard } from '@nestjs/passport';
|
|
|
4
4
|
@Injectable()
|
|
5
5
|
export class GithubAuthGuard extends AuthGuard('github') {
|
|
6
6
|
constructor() {
|
|
7
|
-
// API
|
|
7
|
+
// Stateless API (Bearer token) — Passport/Express sessions are not used.
|
|
8
8
|
super({ session: false });
|
|
9
9
|
}
|
|
10
|
-
}
|
|
10
|
+
}
|
|
@@ -4,7 +4,7 @@ import { AuthGuard } from '@nestjs/passport';
|
|
|
4
4
|
@Injectable()
|
|
5
5
|
export class GoogleAuthGuard extends AuthGuard('google') {
|
|
6
6
|
constructor() {
|
|
7
|
-
// API
|
|
7
|
+
// Stateless API (Bearer token) — Passport/Express sessions are not used.
|
|
8
8
|
super({ session: false });
|
|
9
9
|
}
|
|
10
|
-
}
|
|
10
|
+
}
|
|
@@ -44,14 +44,14 @@ describe('SessionAuthGuard', () => {
|
|
|
44
44
|
return { context, request };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
it('
|
|
47
|
+
it('allows access to public routes without a session', () => {
|
|
48
48
|
reflector.getAllAndOverride.mockReturnValue(true);
|
|
49
49
|
const { context } = createContext();
|
|
50
50
|
|
|
51
51
|
expect(guard.canActivate(context)).toBe(true);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it('
|
|
54
|
+
it('rejects access when the session has no user', () => {
|
|
55
55
|
reflector.getAllAndOverride.mockReturnValue(false);
|
|
56
56
|
const { context } = createContext();
|
|
57
57
|
|
|
@@ -60,7 +60,7 @@ describe('SessionAuthGuard', () => {
|
|
|
60
60
|
);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
it('
|
|
63
|
+
it('allows access and exposes the authenticated user', () => {
|
|
64
64
|
reflector.getAllAndOverride.mockReturnValue(false);
|
|
65
65
|
|
|
66
66
|
const user = {
|
|
@@ -74,4 +74,4 @@ describe('SessionAuthGuard', () => {
|
|
|
74
74
|
expect(guard.canActivate(context)).toBe(true);
|
|
75
75
|
expect(request.user).toEqual(user);
|
|
76
76
|
});
|
|
77
|
-
});
|
|
77
|
+
});
|
|
@@ -27,11 +27,11 @@ export class SessionAuthGuard implements CanActivate {
|
|
|
27
27
|
const user = request.session?.user;
|
|
28
28
|
|
|
29
29
|
if (!user) {
|
|
30
|
-
throw new UnauthorizedException('
|
|
30
|
+
throw new UnauthorizedException('Invalid or expired session');
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
request.user = user;
|
|
34
34
|
|
|
35
35
|
return true;
|
|
36
36
|
}
|
|
37
|
-
}
|
|
37
|
+
}
|
|
@@ -31,7 +31,7 @@ describe('SessionService', () => {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
it('
|
|
34
|
+
it('regenerates, populates, and saves the session', async () => {
|
|
35
35
|
const { request, session } = createRequest();
|
|
36
36
|
const user: SessionUser = {
|
|
37
37
|
id: 'user-1',
|
|
@@ -51,7 +51,7 @@ describe('SessionService', () => {
|
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it('
|
|
54
|
+
it('issues and saves a new CSRF token', async () => {
|
|
55
55
|
const { request, session } = createRequest();
|
|
56
56
|
|
|
57
57
|
const result = await sessionService.issueCsrfToken(request);
|
|
@@ -61,14 +61,14 @@ describe('SessionService', () => {
|
|
|
61
61
|
expect(session.save).toHaveBeenCalledOnce();
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
it('
|
|
64
|
+
it('destroys the current session', async () => {
|
|
65
65
|
const { request, session } = createRequest();
|
|
66
66
|
|
|
67
67
|
const result = await sessionService.destroy(request);
|
|
68
68
|
|
|
69
69
|
expect(session.destroy).toHaveBeenCalledOnce();
|
|
70
70
|
expect(result).toEqual({
|
|
71
|
-
message: 'Logout
|
|
71
|
+
message: 'Logout completed successfully',
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
|
-
});
|
|
74
|
+
});
|
|
@@ -47,7 +47,7 @@ export class SessionService {
|
|
|
47
47
|
if (error) {
|
|
48
48
|
reject(
|
|
49
49
|
new InternalServerErrorException(
|
|
50
|
-
'
|
|
50
|
+
'Unable to end the session',
|
|
51
51
|
),
|
|
52
52
|
);
|
|
53
53
|
return;
|
|
@@ -57,7 +57,7 @@ export class SessionService {
|
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
return { message: 'Logout
|
|
60
|
+
return { message: 'Logout completed successfully' };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
private async regenerate(request: Request): Promise<void> {
|
|
@@ -66,7 +66,7 @@ export class SessionService {
|
|
|
66
66
|
if (error) {
|
|
67
67
|
reject(
|
|
68
68
|
new InternalServerErrorException(
|
|
69
|
-
'
|
|
69
|
+
'Unable to start the session',
|
|
70
70
|
),
|
|
71
71
|
);
|
|
72
72
|
return;
|
|
@@ -83,7 +83,7 @@ export class SessionService {
|
|
|
83
83
|
if (error) {
|
|
84
84
|
reject(
|
|
85
85
|
new InternalServerErrorException(
|
|
86
|
-
'
|
|
86
|
+
'Unable to save the session',
|
|
87
87
|
),
|
|
88
88
|
);
|
|
89
89
|
return;
|
|
@@ -93,4 +93,4 @@ export class SessionService {
|
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
|
-
}
|
|
96
|
+
}
|
|
@@ -25,7 +25,7 @@ export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
|
|
|
25
25
|
profile: Profile,
|
|
26
26
|
done: (error: unknown, user?: OAuthProfile) => void,
|
|
27
27
|
) {
|
|
28
|
-
//
|
|
28
|
+
// The email may be missing when the user keeps it private on GitHub.
|
|
29
29
|
const email =
|
|
30
30
|
profile.emails?.[0]?.value ?? `${profile.username}@users.noreply.github.com`;
|
|
31
31
|
|
|
@@ -33,9 +33,9 @@ export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
|
|
|
33
33
|
provider: 'github',
|
|
34
34
|
providerUserId: profile.id,
|
|
35
35
|
email,
|
|
36
|
-
name: profile.displayName || profile.username || '
|
|
36
|
+
name: profile.displayName || profile.username || 'GitHub User',
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
done(null, oauthProfile);
|
|
40
40
|
}
|
|
41
|
-
}
|
|
41
|
+
}
|
|
@@ -120,7 +120,7 @@ export class TokenService {
|
|
|
120
120
|
stored.expiresAt < new Date()
|
|
121
121
|
) {
|
|
122
122
|
throw new UnauthorizedException(
|
|
123
|
-
'
|
|
123
|
+
'Invalid or expired refresh token',
|
|
124
124
|
);
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -163,7 +163,7 @@ export class TokenService {
|
|
|
163
163
|
);
|
|
164
164
|
|
|
165
165
|
return {
|
|
166
|
-
message: 'Logout
|
|
166
|
+
message: 'Logout completed successfully',
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -172,4 +172,4 @@ export class TokenService {
|
|
|
172
172
|
.update(token)
|
|
173
173
|
.digest('hex');
|
|
174
174
|
}
|
|
175
|
-
}
|
|
175
|
+
}
|
|
@@ -22,7 +22,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
|
|
22
22
|
const message =
|
|
23
23
|
exception instanceof HttpException
|
|
24
24
|
? exception.getResponse()
|
|
25
|
-
: '
|
|
25
|
+
: 'Internal server error';
|
|
26
26
|
|
|
27
27
|
response.status(status).json({
|
|
28
28
|
statusCode: status,
|
|
@@ -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
|
+
}
|