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
|
@@ -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
|
+
}
|
|
@@ -37,35 +37,35 @@ describe('UsersService', () => {
|
|
|
37
37
|
usersService = new UsersService(prisma as PrismaService);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
it('
|
|
40
|
+
it('throws ConflictException when the email already exists during creation', async () => {
|
|
41
41
|
prisma.user.findUnique.mockResolvedValue(mockUser);
|
|
42
42
|
|
|
43
43
|
await expect(
|
|
44
|
-
usersService.create({ name: 'Jeiel', email: 'jeiel@example.com', password: '
|
|
44
|
+
usersService.create({ name: 'Jeiel', email: 'jeiel@example.com', password: 'strongPassword123' }),
|
|
45
45
|
).rejects.toBeInstanceOf(ConflictException);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
it('
|
|
48
|
+
it('creates a user and hides passwordHash during serialization', async () => {
|
|
49
49
|
prisma.user.findUnique.mockResolvedValue(null);
|
|
50
50
|
prisma.user.create.mockResolvedValue(mockUser);
|
|
51
51
|
|
|
52
52
|
const result = await usersService.create({
|
|
53
53
|
name: 'Jeiel',
|
|
54
54
|
email: 'jeiel@example.com',
|
|
55
|
-
password: '
|
|
55
|
+
password: 'strongPassword123',
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
expect(result.email).toBe('jeiel@example.com');
|
|
59
59
|
expect(instanceToPlain(result)).not.toHaveProperty('passwordHash');
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it('
|
|
62
|
+
it('throws NotFoundException when the user does not exist', async () => {
|
|
63
63
|
prisma.user.findUnique.mockResolvedValue(null);
|
|
64
64
|
|
|
65
65
|
await expect(usersService.findOne('id-invalido')).rejects.toBeInstanceOf(NotFoundException);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('
|
|
68
|
+
it('returns paginated data from findAll', async () => {
|
|
69
69
|
prisma.user.findMany.mockResolvedValue([mockUser]);
|
|
70
70
|
prisma.user.count.mockResolvedValue(1);
|
|
71
71
|
|
|
@@ -75,7 +75,7 @@ describe('UsersService', () => {
|
|
|
75
75
|
expect(result.data).toHaveLength(1);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
it('
|
|
78
|
+
it('updates an existing user', async () => {
|
|
79
79
|
prisma.user.findUnique.mockResolvedValue(mockUser);
|
|
80
80
|
prisma.user.update.mockResolvedValue({ ...mockUser, name: 'Novo Nome' });
|
|
81
81
|
|
|
@@ -84,16 +84,16 @@ describe('UsersService', () => {
|
|
|
84
84
|
expect(result.name).toBe('Novo Nome');
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
it('
|
|
87
|
+
it('deletes an existing user', async () => {
|
|
88
88
|
prisma.user.findUnique.mockResolvedValue(mockUser);
|
|
89
89
|
prisma.user.delete.mockResolvedValue(mockUser);
|
|
90
90
|
|
|
91
91
|
const result = await usersService.remove('user-1');
|
|
92
92
|
|
|
93
|
-
expect(result).toEqual({ message: '
|
|
93
|
+
expect(result).toEqual({ message: 'User deleted successfully' });
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
it('
|
|
96
|
+
it('updates a user avatar', async () => {
|
|
97
97
|
prisma.user.findUnique.mockResolvedValue(mockUser);
|
|
98
98
|
prisma.user.update.mockResolvedValue({ ...mockUser, avatarUrl: '/uploads/avatars/x.png' });
|
|
99
99
|
|
|
@@ -101,4 +101,4 @@ describe('UsersService', () => {
|
|
|
101
101
|
|
|
102
102
|
expect(result.avatarUrl).toBe('/uploads/avatars/x.png');
|
|
103
103
|
});
|
|
104
|
-
});
|
|
104
|
+
});
|
|
@@ -14,7 +14,7 @@ export class UsersService {
|
|
|
14
14
|
async create(dto: CreateUserDto) {
|
|
15
15
|
const existing = await this.prisma.user.findUnique({ where: { email: dto.email } });
|
|
16
16
|
if (existing) {
|
|
17
|
-
throw new ConflictException('
|
|
17
|
+
throw new ConflictException('Email already registered');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const passwordHash = await bcrypt.hash(dto.password, 10);
|
|
@@ -79,7 +79,7 @@ export class UsersService {
|
|
|
79
79
|
async findOne(id: string) {
|
|
80
80
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
|
81
81
|
if (!user) {
|
|
82
|
-
throw new NotFoundException('
|
|
82
|
+
throw new NotFoundException('User not found');
|
|
83
83
|
}
|
|
84
84
|
return new UserEntity(user);
|
|
85
85
|
}
|
|
@@ -100,7 +100,7 @@ export class UsersService {
|
|
|
100
100
|
async remove(id: string) {
|
|
101
101
|
await this.findOne(id);
|
|
102
102
|
await this.prisma.user.delete({ where: { id } });
|
|
103
|
-
return { message: '
|
|
103
|
+
return { message: 'User deleted successfully' };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
async updateAvatar(id: string, avatarUrl: string) {
|
|
@@ -111,4 +111,4 @@ export class UsersService {
|
|
|
111
111
|
});
|
|
112
112
|
return new UserEntity(user);
|
|
113
113
|
}
|
|
114
|
-
}
|
|
114
|
+
}
|
|
@@ -23,12 +23,12 @@ describe('Auth (e2e)', () => {
|
|
|
23
23
|
await app.close();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
it('
|
|
26
|
+
it('registers, logs in, refreshes, and revokes the token', async () => {
|
|
27
27
|
const server = app.getHttpServer();
|
|
28
28
|
|
|
29
29
|
const registerResponse = await request(server)
|
|
30
30
|
.post('/auth/register')
|
|
31
|
-
.send({ name: 'Jeiel', email: 'jeiel.e2e@example.com', password: '
|
|
31
|
+
.send({ name: 'Jeiel', email: 'jeiel.e2e@example.com', password: 'strongPassword123' })
|
|
32
32
|
.expect(201);
|
|
33
33
|
|
|
34
34
|
expect(registerResponse.body).toHaveProperty('accessToken');
|
|
@@ -36,7 +36,7 @@ describe('Auth (e2e)', () => {
|
|
|
36
36
|
|
|
37
37
|
const loginResponse = await request(server)
|
|
38
38
|
.post('/auth/login')
|
|
39
|
-
.send({ email: 'jeiel.e2e@example.com', password: '
|
|
39
|
+
.send({ email: 'jeiel.e2e@example.com', password: 'strongPassword123' })
|
|
40
40
|
.expect(200);
|
|
41
41
|
|
|
42
42
|
const refreshResponse = await request(server)
|
|
@@ -51,25 +51,25 @@ describe('Auth (e2e)', () => {
|
|
|
51
51
|
.send({ refreshToken: refreshResponse.body.refreshToken })
|
|
52
52
|
.expect(200);
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// The refresh token was revoked during logout and cannot be reused.
|
|
55
55
|
await request(server)
|
|
56
56
|
.post('/auth/refresh')
|
|
57
57
|
.send({ refreshToken: refreshResponse.body.refreshToken })
|
|
58
58
|
.expect(401);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
it('
|
|
61
|
+
it('rejects registration with a duplicate email', async () => {
|
|
62
62
|
const server = app.getHttpServer();
|
|
63
|
-
const payload = { name: 'Jeiel', email: '
|
|
63
|
+
const payload = { name: 'Jeiel', email: 'duplicate.e2e@example.com', password: 'strongPassword123' };
|
|
64
64
|
|
|
65
65
|
await request(server).post('/auth/register').send(payload).expect(201);
|
|
66
66
|
await request(server).post('/auth/register').send(payload).expect(409);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it('
|
|
69
|
+
it('rejects login with invalid credentials', async () => {
|
|
70
70
|
await request(app.getHttpServer())
|
|
71
71
|
.post('/auth/login')
|
|
72
72
|
.send({ email: 'naoexiste.e2e@example.com', password: 'qualquerSenha123' })
|
|
73
73
|
.expect(401);
|
|
74
74
|
});
|
|
75
|
-
});
|
|
75
|
+
});
|
|
@@ -28,7 +28,7 @@ describe('Session auth (e2e)', () => {
|
|
|
28
28
|
}
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
it('
|
|
31
|
+
it('registers, authenticates, and closes a CSRF-protected session', async () => {
|
|
32
32
|
const agent = request.agent(app.getHttpServer());
|
|
33
33
|
|
|
34
34
|
await agent
|
|
@@ -43,13 +43,13 @@ describe('Session auth (e2e)', () => {
|
|
|
43
43
|
|
|
44
44
|
expect(csrfToken).toMatch(/^[a-f0-9]{64}$/);
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// State-changing requests must be rejected without the token.
|
|
47
47
|
await agent
|
|
48
48
|
.post('/auth/register')
|
|
49
49
|
.send({
|
|
50
50
|
name: 'Jeiel',
|
|
51
51
|
email: 'jeiel.session@example.com',
|
|
52
|
-
password: '
|
|
52
|
+
password: 'strongPassword123',
|
|
53
53
|
})
|
|
54
54
|
.expect(403);
|
|
55
55
|
|
|
@@ -59,7 +59,7 @@ describe('Session auth (e2e)', () => {
|
|
|
59
59
|
.send({
|
|
60
60
|
name: 'Jeiel',
|
|
61
61
|
email: 'jeiel.session@example.com',
|
|
62
|
-
password: '
|
|
62
|
+
password: 'strongPassword123',
|
|
63
63
|
})
|
|
64
64
|
.expect(201);
|
|
65
65
|
|
|
@@ -97,7 +97,7 @@ describe('Session auth (e2e)', () => {
|
|
|
97
97
|
.expect(401);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
it('
|
|
100
|
+
it('logs in and refreshes the session CSRF token', async () => {
|
|
101
101
|
const registrationAgent = request.agent(
|
|
102
102
|
app.getHttpServer(),
|
|
103
103
|
);
|
|
@@ -113,9 +113,9 @@ describe('Session auth (e2e)', () => {
|
|
|
113
113
|
.post('/auth/register')
|
|
114
114
|
.set('x-csrf-token', registrationCsrfToken)
|
|
115
115
|
.send({
|
|
116
|
-
name: '
|
|
116
|
+
name: 'Session User',
|
|
117
117
|
email: 'login.session@example.com',
|
|
118
|
-
password: '
|
|
118
|
+
password: 'strongPassword123',
|
|
119
119
|
})
|
|
120
120
|
.expect(201);
|
|
121
121
|
|
|
@@ -141,7 +141,7 @@ describe('Session auth (e2e)', () => {
|
|
|
141
141
|
.set('x-csrf-token', loginCsrfToken)
|
|
142
142
|
.send({
|
|
143
143
|
email: 'login.session@example.com',
|
|
144
|
-
password: '
|
|
144
|
+
password: 'strongPassword123',
|
|
145
145
|
})
|
|
146
146
|
.expect(200);
|
|
147
147
|
|
|
@@ -160,4 +160,4 @@ describe('Session auth (e2e)', () => {
|
|
|
160
160
|
.get('/users/me')
|
|
161
161
|
.expect(200);
|
|
162
162
|
});
|
|
163
|
-
});
|
|
163
|
+
});
|
|
@@ -13,7 +13,7 @@ describe('Users (e2e)', () => {
|
|
|
13
13
|
async function createUserWithRole(email: string, password: string, role: Role) {
|
|
14
14
|
const passwordHash = await bcrypt.hash(password, 10);
|
|
15
15
|
await prisma.user.create({
|
|
16
|
-
data: { name: '
|
|
16
|
+
data: { name: 'Test User', email, passwordHash, role, emailVerifiedAt: new Date() },
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -39,19 +39,19 @@ describe('Users (e2e)', () => {
|
|
|
39
39
|
await app.close();
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it('
|
|
42
|
+
it('rejects access without a token', async () => {
|
|
43
43
|
await request(app.getHttpServer()).get('/users').expect(401);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
it('ADMIN
|
|
47
|
-
await createUserWithRole('admin.e2e@example.com', '
|
|
48
|
-
const token = await loginAndGetToken('admin.e2e@example.com', '
|
|
46
|
+
it('allows ADMIN to create, list, update, and delete a user', async () => {
|
|
47
|
+
await createUserWithRole('admin.e2e@example.com', 'strongPassword123', Role.ADMIN);
|
|
48
|
+
const token = await loginAndGetToken('admin.e2e@example.com', 'strongPassword123');
|
|
49
49
|
const server = app.getHttpServer();
|
|
50
50
|
|
|
51
51
|
const createResponse = await request(server)
|
|
52
52
|
.post('/users')
|
|
53
53
|
.set('Authorization', `Bearer ${token}`)
|
|
54
|
-
.send({ name: '
|
|
54
|
+
.send({ name: 'New User', email: 'new.e2e@example.com', password: 'strongPassword123' })
|
|
55
55
|
.expect(201);
|
|
56
56
|
|
|
57
57
|
expect(createResponse.body).not.toHaveProperty('passwordHash');
|
|
@@ -77,9 +77,9 @@ describe('Users (e2e)', () => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
// nestforge:feature:rbac
|
|
80
|
-
it('USER
|
|
81
|
-
await createUserWithRole('user.e2e@example.com', '
|
|
82
|
-
const token = await loginAndGetToken('user.e2e@example.com', '
|
|
80
|
+
it('allows USER to read but not create users', async () => {
|
|
81
|
+
await createUserWithRole('user.e2e@example.com', 'strongPassword123', Role.USER);
|
|
82
|
+
const token = await loginAndGetToken('user.e2e@example.com', 'strongPassword123');
|
|
83
83
|
const server = app.getHttpServer();
|
|
84
84
|
|
|
85
85
|
await request(server).get('/users').set('Authorization', `Bearer ${token}`).expect(200);
|
|
@@ -87,14 +87,14 @@ describe('Users (e2e)', () => {
|
|
|
87
87
|
await request(server)
|
|
88
88
|
.post('/users')
|
|
89
89
|
.set('Authorization', `Bearer ${token}`)
|
|
90
|
-
.send({ name: '
|
|
90
|
+
.send({ name: 'Should Not Be Created', email: 'blocked.e2e@example.com', password: 'strongPassword123' })
|
|
91
91
|
.expect(403);
|
|
92
92
|
});
|
|
93
93
|
// nestforge:feature:rbac:end
|
|
94
94
|
|
|
95
|
-
it('GET /users/me
|
|
96
|
-
await createUserWithRole('me.e2e@example.com', '
|
|
97
|
-
const token = await loginAndGetToken('me.e2e@example.com', '
|
|
95
|
+
it('returns the authenticated user from GET /users/me', async () => {
|
|
96
|
+
await createUserWithRole('me.e2e@example.com', 'strongPassword123', Role.USER);
|
|
97
|
+
const token = await loginAndGetToken('me.e2e@example.com', 'strongPassword123');
|
|
98
98
|
|
|
99
99
|
const response = await request(app.getHttpServer())
|
|
100
100
|
.get('/users/me')
|
|
@@ -103,4 +103,4 @@ describe('Users (e2e)', () => {
|
|
|
103
103
|
|
|
104
104
|
expect(response.body.email).toBe('me.e2e@example.com');
|
|
105
105
|
});
|
|
106
|
-
});
|
|
106
|
+
});
|
|
@@ -52,10 +52,10 @@ export class AuthController {
|
|
|
52
52
|
@Public()
|
|
53
53
|
@Get('csrf-token')
|
|
54
54
|
// nestforge:feature:swagger
|
|
55
|
-
@ApiOperation({ summary: '
|
|
55
|
+
@ApiOperation({ summary: 'Issues a CSRF token bound to the session' })
|
|
56
56
|
@ApiResponse({
|
|
57
57
|
status: 200,
|
|
58
|
-
description: '
|
|
58
|
+
description: 'CSRF token issued',
|
|
59
59
|
schema: {
|
|
60
60
|
example: {
|
|
61
61
|
csrfToken: 'a1b2c3d4e5f6...',
|
|
@@ -72,9 +72,9 @@ export class AuthController {
|
|
|
72
72
|
@Public()
|
|
73
73
|
@Post('register')
|
|
74
74
|
// nestforge:feature:swagger
|
|
75
|
-
@ApiOperation({ summary: '
|
|
76
|
-
@ApiResponse({ status: 201, description: '
|
|
77
|
-
@ApiResponse({ status: 409, description: '
|
|
75
|
+
@ApiOperation({ summary: 'Creates a new account' })
|
|
76
|
+
@ApiResponse({ status: 201, description: 'Account created successfully', schema: { example: TOKENS_EXAMPLE } })
|
|
77
|
+
@ApiResponse({ status: 409, description: 'Email already registered', schema: { example: { statusCode: 409, message: 'Email already registered' } } })
|
|
78
78
|
// nestforge:feature:swagger:end
|
|
79
79
|
async register(@Body() dto: RegisterDto) {
|
|
80
80
|
const user = await this.authService.register(dto);
|
|
@@ -85,9 +85,9 @@ export class AuthController {
|
|
|
85
85
|
@Post('login')
|
|
86
86
|
@HttpCode(HttpStatus.OK)
|
|
87
87
|
// nestforge:feature:swagger
|
|
88
|
-
@ApiOperation({ summary: '
|
|
89
|
-
@ApiResponse({ status: 200, description: 'Login
|
|
90
|
-
@ApiResponse({ status: 401, description: '
|
|
88
|
+
@ApiOperation({ summary: 'Authenticates and returns access and refresh tokens' })
|
|
89
|
+
@ApiResponse({ status: 200, description: 'Login completed successfully', schema: { example: TOKENS_EXAMPLE } })
|
|
90
|
+
@ApiResponse({ status: 401, description: 'Invalid credentials', schema: { example: { statusCode: 401, message: 'Invalid credentials' } } })
|
|
91
91
|
// nestforge:feature:swagger:end
|
|
92
92
|
async login(@Body() dto: LoginDto) {
|
|
93
93
|
const user = await this.authService.login(dto);
|
|
@@ -99,9 +99,9 @@ export class AuthController {
|
|
|
99
99
|
@Public()
|
|
100
100
|
@Post('register')
|
|
101
101
|
// nestforge:feature:swagger
|
|
102
|
-
@ApiOperation({ summary: '
|
|
103
|
-
@ApiResponse({ status: 201, description: '
|
|
104
|
-
@ApiResponse({ status: 409, description: '
|
|
102
|
+
@ApiOperation({ summary: 'Creates an account and starts a session' })
|
|
103
|
+
@ApiResponse({ status: 201, description: 'Account created and session started' })
|
|
104
|
+
@ApiResponse({ status: 409, description: 'Email already registered' })
|
|
105
105
|
// nestforge:feature:swagger:end
|
|
106
106
|
async registerWithSession(
|
|
107
107
|
@Body() dto: RegisterDto,
|
|
@@ -115,9 +115,9 @@ export class AuthController {
|
|
|
115
115
|
@Post('login')
|
|
116
116
|
@HttpCode(HttpStatus.OK)
|
|
117
117
|
// nestforge:feature:swagger
|
|
118
|
-
@ApiOperation({ summary: '
|
|
119
|
-
@ApiResponse({ status: 200, description: '
|
|
120
|
-
@ApiResponse({ status: 401, description: '
|
|
118
|
+
@ApiOperation({ summary: 'Authenticates and starts a cookie-based session' })
|
|
119
|
+
@ApiResponse({ status: 200, description: 'Session started successfully' })
|
|
120
|
+
@ApiResponse({ status: 401, description: 'Invalid credentials' })
|
|
121
121
|
// nestforge:feature:swagger:end
|
|
122
122
|
async loginWithSession(
|
|
123
123
|
@Body() dto: LoginDto,
|
|
@@ -130,8 +130,8 @@ export class AuthController {
|
|
|
130
130
|
@Post('logout')
|
|
131
131
|
@HttpCode(HttpStatus.OK)
|
|
132
132
|
// nestforge:feature:swagger
|
|
133
|
-
@ApiOperation({ summary: '
|
|
134
|
-
@ApiResponse({ status: 200, description: '
|
|
133
|
+
@ApiOperation({ summary: 'Ends the current session' })
|
|
134
|
+
@ApiResponse({ status: 200, description: 'Session ended successfully' })
|
|
135
135
|
// nestforge:feature:swagger:end
|
|
136
136
|
logoutSession(@Req() request: Request) {
|
|
137
137
|
return this.sessionService.destroy(request);
|
|
@@ -143,9 +143,9 @@ export class AuthController {
|
|
|
143
143
|
@Post('refresh')
|
|
144
144
|
@HttpCode(HttpStatus.OK)
|
|
145
145
|
// nestforge:feature:swagger
|
|
146
|
-
@ApiOperation({ summary: '
|
|
147
|
-
@ApiResponse({ status: 200, description: '
|
|
148
|
-
@ApiResponse({ status: 401, description: '
|
|
146
|
+
@ApiOperation({ summary: 'Renews the access token using the refresh token' })
|
|
147
|
+
@ApiResponse({ status: 200, description: 'New token pair issued', schema: { example: TOKENS_EXAMPLE } })
|
|
148
|
+
@ApiResponse({ status: 401, description: 'Invalid, expired, or already used refresh token', schema: { example: { statusCode: 401, message: 'Invalid or expired refresh token' } } })
|
|
149
149
|
// nestforge:feature:swagger:end
|
|
150
150
|
refresh(@Body() dto: RefreshTokenDto) {
|
|
151
151
|
return this.tokenService.refresh(dto.refreshToken);
|
|
@@ -155,8 +155,8 @@ export class AuthController {
|
|
|
155
155
|
@Post('logout')
|
|
156
156
|
@HttpCode(HttpStatus.OK)
|
|
157
157
|
// nestforge:feature:swagger
|
|
158
|
-
@ApiOperation({ summary: '
|
|
159
|
-
@ApiResponse({ status: 200, description: 'Logout
|
|
158
|
+
@ApiOperation({ summary: 'Revokes the provided refresh token' })
|
|
159
|
+
@ApiResponse({ status: 200, description: 'Logout completed', schema: { example: { message: 'Logout completed successfully' } } })
|
|
160
160
|
// nestforge:feature:swagger:end
|
|
161
161
|
logout(@Body() dto: RefreshTokenDto) {
|
|
162
162
|
return this.tokenService.logout(dto.refreshToken);
|
|
@@ -168,11 +168,11 @@ export class AuthController {
|
|
|
168
168
|
@Post('forgot-password')
|
|
169
169
|
@HttpCode(HttpStatus.OK)
|
|
170
170
|
// nestforge:feature:swagger
|
|
171
|
-
@ApiOperation({ summary: '
|
|
171
|
+
@ApiOperation({ summary: 'Sends an email with password reset instructions' })
|
|
172
172
|
@ApiResponse({
|
|
173
173
|
status: 200,
|
|
174
|
-
description: '
|
|
175
|
-
schema: { example: { message: '
|
|
174
|
+
description: 'Generic response — always the same whether the email exists or not (prevents account enumeration)',
|
|
175
|
+
schema: { example: { message: 'If the email exists, password reset instructions will be sent' } },
|
|
176
176
|
})
|
|
177
177
|
// nestforge:feature:swagger:end
|
|
178
178
|
forgotPassword(@Body() dto: ForgotPasswordDto) {
|
|
@@ -183,9 +183,9 @@ export class AuthController {
|
|
|
183
183
|
@Post('reset-password')
|
|
184
184
|
@HttpCode(HttpStatus.OK)
|
|
185
185
|
// nestforge:feature:swagger
|
|
186
|
-
@ApiOperation({ summary: '
|
|
187
|
-
@ApiResponse({ status: 200, description: '
|
|
188
|
-
@ApiResponse({ status: 401, description: '
|
|
186
|
+
@ApiOperation({ summary: 'Resets the password using the token received by email' })
|
|
187
|
+
@ApiResponse({ status: 200, description: 'Password reset', schema: { example: { message: 'Password reset successfully' } } })
|
|
188
|
+
@ApiResponse({ status: 401, description: 'Invalid, expired, or already used token', schema: { example: { statusCode: 401, message: 'Invalid or expired password reset token' } } })
|
|
189
189
|
// nestforge:feature:swagger:end
|
|
190
190
|
resetPassword(@Body() dto: ResetPasswordDto) {
|
|
191
191
|
return this.authService.resetPassword(dto.token, dto.password);
|
|
@@ -194,9 +194,9 @@ export class AuthController {
|
|
|
194
194
|
@Public()
|
|
195
195
|
@Get('verify-email')
|
|
196
196
|
// nestforge:feature:swagger
|
|
197
|
-
@ApiOperation({ summary: '
|
|
198
|
-
@ApiResponse({ status: 200, description: '
|
|
199
|
-
@ApiResponse({ status: 401, description: '
|
|
197
|
+
@ApiOperation({ summary: 'Verifies the email using the received token' })
|
|
198
|
+
@ApiResponse({ status: 200, description: 'Email verified', schema: { example: { message: 'Email verified successfully' } } })
|
|
199
|
+
@ApiResponse({ status: 401, description: 'Invalid, expired, or already used token', schema: { example: { statusCode: 401, message: 'Invalid or expired verification token' } } })
|
|
200
200
|
// nestforge:feature:swagger:end
|
|
201
201
|
verifyEmail(@Query('token') token: string) {
|
|
202
202
|
return this.authService.verifyEmail(token);
|
|
@@ -207,10 +207,10 @@ export class AuthController {
|
|
|
207
207
|
@Get('google')
|
|
208
208
|
@UseGuards(GoogleAuthGuard)
|
|
209
209
|
// nestforge:feature:swagger
|
|
210
|
-
@ApiOperation({ summary: '
|
|
210
|
+
@ApiOperation({ summary: 'Starts Google login (redirects)' })
|
|
211
211
|
// nestforge:feature:swagger:end
|
|
212
212
|
googleLogin() {
|
|
213
|
-
//
|
|
213
|
+
// The guard redirects to Google's consent screen; nothing to do here.
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
// nestforge:feature:auth:token
|
|
@@ -246,10 +246,10 @@ export class AuthController {
|
|
|
246
246
|
@Get('github')
|
|
247
247
|
@UseGuards(GithubAuthGuard)
|
|
248
248
|
// nestforge:feature:swagger
|
|
249
|
-
@ApiOperation({ summary: '
|
|
249
|
+
@ApiOperation({ summary: 'Starts GitHub login (redirects)' })
|
|
250
250
|
// nestforge:feature:swagger:end
|
|
251
251
|
githubLogin() {
|
|
252
|
-
//
|
|
252
|
+
// The guard redirects to GitHub's consent screen; nothing to do here.
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
// nestforge:feature:auth:token
|
|
@@ -280,4 +280,4 @@ export class AuthController {
|
|
|
280
280
|
return this.sessionService.establish(request, user);
|
|
281
281
|
}
|
|
282
282
|
// nestforge:feature:auth:session:end
|
|
283
|
-
}
|
|
283
|
+
}
|
|
@@ -81,7 +81,7 @@ describe('AuthService', () => {
|
|
|
81
81
|
);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
it('
|
|
84
|
+
it('throws ConflictException when the email already exists', async () => {
|
|
85
85
|
usersRepository.findOne.mockResolvedValue({
|
|
86
86
|
id: 'user-1',
|
|
87
87
|
email: 'jeiel@example.com',
|
|
@@ -91,12 +91,12 @@ describe('AuthService', () => {
|
|
|
91
91
|
authService.register({
|
|
92
92
|
name: 'Jeiel',
|
|
93
93
|
email: 'jeiel@example.com',
|
|
94
|
-
password: '
|
|
94
|
+
password: 'strongPassword123',
|
|
95
95
|
}),
|
|
96
96
|
).rejects.toBeInstanceOf(ConflictException);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
it('
|
|
99
|
+
it('throws UnauthorizedException for invalid credentials', async () => {
|
|
100
100
|
usersRepository.findOne.mockResolvedValue(null);
|
|
101
101
|
|
|
102
102
|
await expect(
|
|
@@ -108,4 +108,4 @@ describe('AuthService', () => {
|
|
|
108
108
|
UnauthorizedException,
|
|
109
109
|
);
|
|
110
110
|
});
|
|
111
|
-
});
|
|
111
|
+
});
|