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
|
@@ -60,7 +60,7 @@ export class AuthService {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
if (existing) {
|
|
63
|
-
throw new ConflictException('
|
|
63
|
+
throw new ConflictException('Email already registered');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const passwordHash = await bcrypt.hash(dto.password, 10);
|
|
@@ -91,7 +91,7 @@ export class AuthService {
|
|
|
91
91
|
|
|
92
92
|
if (!user || !user.passwordHash) {
|
|
93
93
|
throw new UnauthorizedException(
|
|
94
|
-
'
|
|
94
|
+
'Invalid credentials',
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -102,7 +102,7 @@ export class AuthService {
|
|
|
102
102
|
|
|
103
103
|
if (!passwordMatches) {
|
|
104
104
|
throw new UnauthorizedException(
|
|
105
|
-
'
|
|
105
|
+
'Invalid credentials',
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -164,7 +164,7 @@ export class AuthService {
|
|
|
164
164
|
|
|
165
165
|
const genericResponse = {
|
|
166
166
|
message:
|
|
167
|
-
'
|
|
167
|
+
'If the email exists, password reset instructions will be sent',
|
|
168
168
|
};
|
|
169
169
|
|
|
170
170
|
if (!user) {
|
|
@@ -214,7 +214,7 @@ export class AuthService {
|
|
|
214
214
|
stored.expiresAt < new Date()
|
|
215
215
|
) {
|
|
216
216
|
throw new UnauthorizedException(
|
|
217
|
-
'
|
|
217
|
+
'Invalid or expired password reset token',
|
|
218
218
|
);
|
|
219
219
|
}
|
|
220
220
|
|
|
@@ -253,7 +253,7 @@ export class AuthService {
|
|
|
253
253
|
);
|
|
254
254
|
|
|
255
255
|
return {
|
|
256
|
-
message: '
|
|
256
|
+
message: 'Password reset successfully',
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -271,7 +271,7 @@ export class AuthService {
|
|
|
271
271
|
stored.expiresAt < new Date()
|
|
272
272
|
) {
|
|
273
273
|
throw new UnauthorizedException(
|
|
274
|
-
'
|
|
274
|
+
'Invalid or expired verification token',
|
|
275
275
|
);
|
|
276
276
|
}
|
|
277
277
|
|
|
@@ -292,7 +292,7 @@ export class AuthService {
|
|
|
292
292
|
);
|
|
293
293
|
|
|
294
294
|
return {
|
|
295
|
-
message: '
|
|
295
|
+
message: 'Email verified successfully',
|
|
296
296
|
};
|
|
297
297
|
}
|
|
298
298
|
|
|
@@ -343,4 +343,4 @@ export class AuthService {
|
|
|
343
343
|
.update(token)
|
|
344
344
|
.digest('hex');
|
|
345
345
|
}
|
|
346
|
-
}
|
|
346
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -76,7 +76,7 @@ export class TokenService {
|
|
|
76
76
|
stored.expiresAt < new Date()
|
|
77
77
|
) {
|
|
78
78
|
throw new UnauthorizedException(
|
|
79
|
-
'
|
|
79
|
+
'Invalid or expired refresh token',
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -104,7 +104,7 @@ export class TokenService {
|
|
|
104
104
|
},
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
-
return { message: 'Logout
|
|
107
|
+
return { message: 'Logout completed successfully' };
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
private hashToken(token: string): string {
|
|
@@ -112,4 +112,4 @@ export class TokenService {
|
|
|
112
112
|
.update(token)
|
|
113
113
|
.digest('hex');
|
|
114
114
|
}
|
|
115
|
-
}
|
|
115
|
+
}
|
|
@@ -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;
|
|
@@ -12,9 +12,9 @@ const databaseType = (
|
|
|
12
12
|
const databaseUrl = process.env.DATABASE_URL;
|
|
13
13
|
|
|
14
14
|
if (!databaseUrl) {
|
|
15
|
-
throw new Error('DATABASE_URL
|
|
15
|
+
throw new Error('DATABASE_URL is not defined');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export default new DataSource(
|
|
19
19
|
createTypeOrmOptions(databaseType, databaseUrl),
|
|
20
|
-
);
|
|
20
|
+
);
|
|
@@ -25,7 +25,7 @@ const SEED_USERS: SeedUser[] = [
|
|
|
25
25
|
role: Role.MANAGER,
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
|
-
name: '
|
|
28
|
+
name: 'User',
|
|
29
29
|
email: 'user@nestforge.dev',
|
|
30
30
|
password: 'user1234',
|
|
31
31
|
role: Role.USER,
|
|
@@ -76,6 +76,6 @@ async function seed() {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
seed().catch((error: unknown) => {
|
|
79
|
-
console.error('
|
|
79
|
+
console.error('Error while running the seed:', error);
|
|
80
80
|
process.exitCode = 1;
|
|
81
|
-
});
|
|
81
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -9,7 +9,7 @@ import { DataSource } from 'typeorm';
|
|
|
9
9
|
import { TypeOrmHealthIndicator } from './typeorm-health.indicator';
|
|
10
10
|
|
|
11
11
|
describe('TypeOrmHealthIndicator', () => {
|
|
12
|
-
it('
|
|
12
|
+
it('returns an up status when the query succeeds', async () => {
|
|
13
13
|
const dataSource = {
|
|
14
14
|
query: vi.fn().mockResolvedValue([{ result: 1 }]),
|
|
15
15
|
};
|
|
@@ -29,12 +29,12 @@ describe('TypeOrmHealthIndicator', () => {
|
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
it('
|
|
32
|
+
it('throws HealthCheckError when the query fails', async () => {
|
|
33
33
|
const dataSource = {
|
|
34
34
|
query: vi
|
|
35
35
|
.fn()
|
|
36
36
|
.mockRejectedValue(
|
|
37
|
-
new Error('
|
|
37
|
+
new Error('connection refused'),
|
|
38
38
|
),
|
|
39
39
|
};
|
|
40
40
|
|
|
@@ -46,4 +46,4 @@ describe('TypeOrmHealthIndicator', () => {
|
|
|
46
46
|
indicator.isHealthy('database'),
|
|
47
47
|
).rejects.toBeInstanceOf(HealthCheckError);
|
|
48
48
|
});
|
|
49
|
-
});
|
|
49
|
+
});
|