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
|
@@ -24,7 +24,7 @@ export class TypeOrmHealthIndicator {
|
|
|
24
24
|
};
|
|
25
25
|
} catch (error) {
|
|
26
26
|
throw new HealthCheckError(
|
|
27
|
-
'
|
|
27
|
+
'Database unavailable',
|
|
28
28
|
{
|
|
29
29
|
[key]: {
|
|
30
30
|
status: 'down',
|
|
@@ -34,4 +34,4 @@ export class TypeOrmHealthIndicator {
|
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
}
|
|
37
|
+
}
|
|
@@ -27,13 +27,13 @@ export class MailProcessor extends WorkerHost {
|
|
|
27
27
|
|
|
28
28
|
switch (job.name) {
|
|
29
29
|
case MailJob.VerifyEmail:
|
|
30
|
-
await this.send(to, '
|
|
30
|
+
await this.send(to, 'Confirm your email', verifyEmailTemplate(name, token));
|
|
31
31
|
break;
|
|
32
32
|
case MailJob.ResetPassword:
|
|
33
|
-
await this.send(to, '
|
|
33
|
+
await this.send(to, 'Password reset', resetPasswordTemplate(name, token));
|
|
34
34
|
break;
|
|
35
35
|
default:
|
|
36
|
-
this.logger.warn(`
|
|
36
|
+
this.logger.warn(`Unknown email job: ${job.name}`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -45,4 +45,4 @@ export class MailProcessor extends WorkerHost {
|
|
|
45
45
|
html,
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|
|
@@ -6,10 +6,10 @@ export function verifyEmailTemplate(name: string, token: string): string {
|
|
|
6
6
|
|
|
7
7
|
return `
|
|
8
8
|
<div style="font-family: sans-serif; line-height: 1.5;">
|
|
9
|
-
<h2>
|
|
10
|
-
<p>
|
|
9
|
+
<h2>Hello, ${name}!</h2>
|
|
10
|
+
<p>Confirm your email by clicking the link below:</p>
|
|
11
11
|
<p><a href="${link}">${link}</a></p>
|
|
12
|
-
<p>
|
|
12
|
+
<p>This link expires in 24 hours.</p>
|
|
13
13
|
</div>
|
|
14
14
|
`;
|
|
15
15
|
}
|
|
@@ -19,10 +19,10 @@ export function resetPasswordTemplate(name: string, token: string): string {
|
|
|
19
19
|
|
|
20
20
|
return `
|
|
21
21
|
<div style="font-family: sans-serif; line-height: 1.5;">
|
|
22
|
-
<h2>
|
|
23
|
-
<p>
|
|
22
|
+
<h2>Hello, ${name}!</h2>
|
|
23
|
+
<p>We received a request to reset your password. Click the link below to continue:</p>
|
|
24
24
|
<p><a href="${link}">${link}</a></p>
|
|
25
|
-
<p>
|
|
25
|
+
<p>If you did not request this, you can ignore this email. The link expires in 1 hour.</p>
|
|
26
26
|
</div>
|
|
27
27
|
`;
|
|
28
|
-
}
|
|
28
|
+
}
|
|
@@ -7,7 +7,7 @@ export class MetricsService {
|
|
|
7
7
|
|
|
8
8
|
readonly httpRequestDuration = new client.Histogram({
|
|
9
9
|
name: 'http_request_duration_seconds',
|
|
10
|
-
help: '
|
|
10
|
+
help: 'HTTP request duration in seconds',
|
|
11
11
|
labelNames: ['method', 'route', 'status_code'],
|
|
12
12
|
buckets: [0.05, 0.1, 0.3, 0.5, 1, 2, 5],
|
|
13
13
|
registers: [this.registry],
|
|
@@ -15,7 +15,7 @@ export class MetricsService {
|
|
|
15
15
|
|
|
16
16
|
readonly httpRequestsTotal = new client.Counter({
|
|
17
17
|
name: 'http_requests_total',
|
|
18
|
-
help: 'Total
|
|
18
|
+
help: 'Total number of HTTP requests received',
|
|
19
19
|
labelNames: ['method', 'route', 'status_code'],
|
|
20
20
|
registers: [this.registry],
|
|
21
21
|
});
|
|
@@ -31,4 +31,4 @@ export class MetricsService {
|
|
|
31
31
|
getContentType(): string {
|
|
32
32
|
return this.registry.contentType;
|
|
33
33
|
}
|
|
34
|
-
}
|
|
34
|
+
}
|
|
@@ -3,10 +3,10 @@ import { createZodDto } from 'nestjs-zod';
|
|
|
3
3
|
import { Role } from '../../common/constants/role.enum';
|
|
4
4
|
|
|
5
5
|
export const createUserSchema = z.object({
|
|
6
|
-
name: z.string().min(2).describe('
|
|
7
|
-
email: z.string().email().describe('
|
|
8
|
-
password: z.string().min(8).describe('
|
|
9
|
-
role: z.nativeEnum(Role).optional().describe('
|
|
6
|
+
name: z.string().min(2).describe('Full name'),
|
|
7
|
+
email: z.string().email().describe('User email'),
|
|
8
|
+
password: z.string().min(8).describe('Password (minimum 8 characters)'),
|
|
9
|
+
role: z.nativeEnum(Role).optional().describe('User role (default: USER)'),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
export class CreateUserDto extends createZodDto(createUserSchema) { }
|
|
12
|
+
export class CreateUserDto extends createZodDto(createUserSchema) { }
|
|
@@ -3,10 +3,10 @@ import { createZodDto } from 'nestjs-zod';
|
|
|
3
3
|
import { Role } from '../../common/constants/role.enum';
|
|
4
4
|
|
|
5
5
|
export const findUsersQuerySchema = z.object({
|
|
6
|
-
page: z.coerce.number().int().min(1).default(1).describe('
|
|
7
|
-
limit: z.coerce.number().int().min(1).max(100).default(10).describe('
|
|
6
|
+
page: z.coerce.number().int().min(1).default(1).describe('Current page'),
|
|
7
|
+
limit: z.coerce.number().int().min(1).max(100).default(10).describe('Items per page (maximum 100)'),
|
|
8
8
|
search: z.string().optional().describe('Busca por nome ou e-mail'),
|
|
9
9
|
role: z.nativeEnum(Role).optional().describe('Filtra por role'),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
export class FindUsersQueryDto extends createZodDto(findUsersQuerySchema) { }
|
|
12
|
+
export class FindUsersQueryDto extends createZodDto(findUsersQuerySchema) { }
|
|
@@ -57,9 +57,9 @@ export class UsersController {
|
|
|
57
57
|
@Permissions(Permission.UserCreate)
|
|
58
58
|
// nestforge:feature:rbac:end
|
|
59
59
|
// nestforge:feature:swagger
|
|
60
|
-
@ApiOperation({ summary: '
|
|
61
|
-
@ApiResponse({ status: 201, description: '
|
|
62
|
-
@ApiResponse({ status: 403, description: '
|
|
60
|
+
@ApiOperation({ summary: 'Creates a user (requires the user:create permission)' })
|
|
61
|
+
@ApiResponse({ status: 201, description: 'User created', schema: { example: USER_EXAMPLE } })
|
|
62
|
+
@ApiResponse({ status: 403, description: 'Missing the user:create permission', schema: { example: FORBIDDEN_EXAMPLE } })
|
|
63
63
|
// nestforge:feature:swagger:end
|
|
64
64
|
create(@Body() dto: CreateUserDto) {
|
|
65
65
|
return this.usersService.create(dto);
|
|
@@ -70,10 +70,10 @@ export class UsersController {
|
|
|
70
70
|
@Permissions(Permission.UserRead)
|
|
71
71
|
// nestforge:feature:rbac:end
|
|
72
72
|
// nestforge:feature:swagger
|
|
73
|
-
@ApiOperation({ summary: '
|
|
73
|
+
@ApiOperation({ summary: 'Lists users with pagination and filters (requires the user:read permission)' })
|
|
74
74
|
@ApiResponse({
|
|
75
75
|
status: 200,
|
|
76
|
-
description: '
|
|
76
|
+
description: 'Paginated list',
|
|
77
77
|
schema: {
|
|
78
78
|
example: {
|
|
79
79
|
data: [USER_EXAMPLE],
|
|
@@ -88,8 +88,8 @@ export class UsersController {
|
|
|
88
88
|
|
|
89
89
|
@Get('me')
|
|
90
90
|
// nestforge:feature:swagger
|
|
91
|
-
@ApiOperation({ summary: '
|
|
92
|
-
@ApiResponse({ status: 200, description: '
|
|
91
|
+
@ApiOperation({ summary: 'Returns the authenticated user' })
|
|
92
|
+
@ApiResponse({ status: 200, description: 'Authenticated user', schema: { example: USER_EXAMPLE } })
|
|
93
93
|
// nestforge:feature:swagger:end
|
|
94
94
|
findMe(@CurrentUser() user: { id: string }) {
|
|
95
95
|
return this.usersService.findOne(user.id);
|
|
@@ -97,17 +97,17 @@ export class UsersController {
|
|
|
97
97
|
|
|
98
98
|
@Post('me/avatar')
|
|
99
99
|
// nestforge:feature:swagger
|
|
100
|
-
@ApiOperation({ summary: '
|
|
100
|
+
@ApiOperation({ summary: 'Uploads an avatar for the authenticated user' })
|
|
101
101
|
@ApiConsumes('multipart/form-data')
|
|
102
102
|
@ApiResponse({
|
|
103
103
|
status: 201,
|
|
104
|
-
description: 'Avatar
|
|
104
|
+
description: 'Avatar updated',
|
|
105
105
|
schema: { example: { ...USER_EXAMPLE, avatarUrl: '/uploads/avatars/f47ac10b.png' } },
|
|
106
106
|
})
|
|
107
107
|
@ApiResponse({
|
|
108
108
|
status: 400,
|
|
109
|
-
description: '
|
|
110
|
-
schema: { example: { statusCode: 400, message: '
|
|
109
|
+
description: 'Unsupported image format or file larger than 2 MB',
|
|
110
|
+
schema: { example: { statusCode: 400, message: 'Unsupported image format (use PNG, JPEG, or WEBP)' } },
|
|
111
111
|
})
|
|
112
112
|
// nestforge:feature:swagger:end
|
|
113
113
|
@UseInterceptors(
|
|
@@ -129,9 +129,9 @@ export class UsersController {
|
|
|
129
129
|
@Permissions(Permission.UserRead)
|
|
130
130
|
// nestforge:feature:rbac:end
|
|
131
131
|
// nestforge:feature:swagger
|
|
132
|
-
@ApiOperation({ summary: '
|
|
133
|
-
@ApiResponse({ status: 200, description: '
|
|
134
|
-
@ApiResponse({ status: 404, description: '
|
|
132
|
+
@ApiOperation({ summary: 'Finds a user by ID (requires the user:read permission)' })
|
|
133
|
+
@ApiResponse({ status: 200, description: 'User found', schema: { example: USER_EXAMPLE } })
|
|
134
|
+
@ApiResponse({ status: 404, description: 'User not found', schema: { example: { statusCode: 404, message: 'User not found' } } })
|
|
135
135
|
// nestforge:feature:swagger:end
|
|
136
136
|
findOne(@Param('id') id: string) {
|
|
137
137
|
return this.usersService.findOne(id);
|
|
@@ -142,9 +142,9 @@ export class UsersController {
|
|
|
142
142
|
@Permissions(Permission.UserUpdate)
|
|
143
143
|
// nestforge:feature:rbac:end
|
|
144
144
|
// nestforge:feature:swagger
|
|
145
|
-
@ApiOperation({ summary: '
|
|
146
|
-
@ApiResponse({ status: 200, description: '
|
|
147
|
-
@ApiResponse({ status: 403, description: '
|
|
145
|
+
@ApiOperation({ summary: 'Updates a user (requires the user:update permission)' })
|
|
146
|
+
@ApiResponse({ status: 200, description: 'User updated', schema: { example: USER_EXAMPLE } })
|
|
147
|
+
@ApiResponse({ status: 403, description: 'Missing the user:update permission', schema: { example: FORBIDDEN_EXAMPLE } })
|
|
148
148
|
// nestforge:feature:swagger:end
|
|
149
149
|
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
|
|
150
150
|
return this.usersService.update(id, dto);
|
|
@@ -155,11 +155,11 @@ export class UsersController {
|
|
|
155
155
|
@Permissions(Permission.UserDelete)
|
|
156
156
|
// nestforge:feature:rbac:end
|
|
157
157
|
// nestforge:feature:swagger
|
|
158
|
-
@ApiOperation({ summary: '
|
|
159
|
-
@ApiResponse({ status: 200, description: '
|
|
160
|
-
@ApiResponse({ status: 403, description: '
|
|
158
|
+
@ApiOperation({ summary: 'Deletes a user (requires the user:delete permission)' })
|
|
159
|
+
@ApiResponse({ status: 200, description: 'User deleted', schema: { example: { message: 'User deleted successfully' } } })
|
|
160
|
+
@ApiResponse({ status: 403, description: 'Missing the user:delete permission', schema: { example: FORBIDDEN_EXAMPLE } })
|
|
161
161
|
// nestforge:feature:swagger:end
|
|
162
162
|
remove(@Param('id') id: string) {
|
|
163
163
|
return this.usersService.remove(id);
|
|
164
164
|
}
|
|
165
|
-
}
|
|
165
|
+
}
|
|
@@ -49,19 +49,19 @@ describe('UsersService', () => {
|
|
|
49
49
|
);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
it('
|
|
52
|
+
it('throws ConflictException when the email already exists during creation', async () => {
|
|
53
53
|
usersRepository.findOne.mockResolvedValue(mockUser);
|
|
54
54
|
|
|
55
55
|
await expect(
|
|
56
56
|
usersService.create({
|
|
57
57
|
name: 'Jeiel',
|
|
58
58
|
email: 'jeiel@example.com',
|
|
59
|
-
password: '
|
|
59
|
+
password: 'strongPassword123',
|
|
60
60
|
}),
|
|
61
61
|
).rejects.toBeInstanceOf(ConflictException);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
it('
|
|
64
|
+
it('creates a user and hides passwordHash during serialization', async () => {
|
|
65
65
|
usersRepository.findOne.mockResolvedValue(null);
|
|
66
66
|
usersRepository.create.mockReturnValue(mockUser);
|
|
67
67
|
usersRepository.save.mockResolvedValue(mockUser);
|
|
@@ -69,7 +69,7 @@ describe('UsersService', () => {
|
|
|
69
69
|
const result = await usersService.create({
|
|
70
70
|
name: 'Jeiel',
|
|
71
71
|
email: 'jeiel@example.com',
|
|
72
|
-
password: '
|
|
72
|
+
password: 'strongPassword123',
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
expect(result.email).toBe('jeiel@example.com');
|
|
@@ -78,7 +78,7 @@ describe('UsersService', () => {
|
|
|
78
78
|
expect(usersRepository.save).toHaveBeenCalledWith(mockUser);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
it('
|
|
81
|
+
it('throws NotFoundException when the user does not exist', async () => {
|
|
82
82
|
usersRepository.findOne.mockResolvedValue(null);
|
|
83
83
|
|
|
84
84
|
await expect(
|
|
@@ -86,7 +86,7 @@ describe('UsersService', () => {
|
|
|
86
86
|
).rejects.toBeInstanceOf(NotFoundException);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
it('
|
|
89
|
+
it('returns paginated data from findAll', async () => {
|
|
90
90
|
queryBuilder.getManyAndCount.mockResolvedValue([[mockUser], 1]);
|
|
91
91
|
|
|
92
92
|
const result = await usersService.findAll({
|
|
@@ -105,7 +105,7 @@ describe('UsersService', () => {
|
|
|
105
105
|
expect(queryBuilder.take).toHaveBeenCalledWith(10);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
it('
|
|
108
|
+
it('applies search and role filters in findAll', async () => {
|
|
109
109
|
queryBuilder.getManyAndCount.mockResolvedValue([[mockUser], 1]);
|
|
110
110
|
|
|
111
111
|
await usersService.findAll({
|
|
@@ -126,7 +126,7 @@ describe('UsersService', () => {
|
|
|
126
126
|
);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
it('
|
|
129
|
+
it('updates an existing user', async () => {
|
|
130
130
|
usersRepository.findOne.mockResolvedValue(mockUser);
|
|
131
131
|
usersRepository.save.mockImplementation(
|
|
132
132
|
async (user: UserEntity) => user,
|
|
@@ -147,7 +147,7 @@ describe('UsersService', () => {
|
|
|
147
147
|
);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
it('
|
|
150
|
+
it('deletes an existing user', async () => {
|
|
151
151
|
usersRepository.findOne.mockResolvedValue(mockUser);
|
|
152
152
|
usersRepository.remove.mockResolvedValue(mockUser);
|
|
153
153
|
|
|
@@ -155,11 +155,11 @@ describe('UsersService', () => {
|
|
|
155
155
|
|
|
156
156
|
expect(usersRepository.remove).toHaveBeenCalledWith(mockUser);
|
|
157
157
|
expect(result).toEqual({
|
|
158
|
-
message: '
|
|
158
|
+
message: 'User deleted successfully',
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
it('
|
|
162
|
+
it('updates a user avatar', async () => {
|
|
163
163
|
usersRepository.findOne.mockResolvedValue(mockUser);
|
|
164
164
|
usersRepository.save.mockImplementation(
|
|
165
165
|
async (user: UserEntity) => user,
|
|
@@ -180,4 +180,4 @@ describe('UsersService', () => {
|
|
|
180
180
|
}),
|
|
181
181
|
);
|
|
182
182
|
});
|
|
183
|
-
});
|
|
183
|
+
});
|
|
@@ -24,7 +24,7 @@ export class UsersService {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
if (existing) {
|
|
27
|
-
throw new ConflictException('
|
|
27
|
+
throw new ConflictException('Email already registered');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const passwordHash = await bcrypt.hash(dto.password, 10);
|
|
@@ -83,7 +83,7 @@ export class UsersService {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
if (!user) {
|
|
86
|
-
throw new NotFoundException('
|
|
86
|
+
throw new NotFoundException('User not found');
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
return new UserEntity(user);
|
|
@@ -109,7 +109,7 @@ export class UsersService {
|
|
|
109
109
|
|
|
110
110
|
await this.usersRepository.remove(user);
|
|
111
111
|
|
|
112
|
-
return { message: '
|
|
112
|
+
return { message: 'User deleted successfully' };
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
async updateAvatar(id: string, avatarUrl: string) {
|
|
@@ -121,4 +121,4 @@ export class UsersService {
|
|
|
121
121
|
|
|
122
122
|
return new UserEntity(updatedUser);
|
|
123
123
|
}
|
|
124
|
-
}
|
|
124
|
+
}
|
|
@@ -22,7 +22,7 @@ describe('Auth (e2e)', () => {
|
|
|
22
22
|
await app.close();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it('
|
|
25
|
+
it('registers, logs in, refreshes, and revokes the token', async () => {
|
|
26
26
|
const server = app.getHttpServer();
|
|
27
27
|
|
|
28
28
|
const registerResponse = await request(server)
|
|
@@ -30,7 +30,7 @@ describe('Auth (e2e)', () => {
|
|
|
30
30
|
.send({
|
|
31
31
|
name: 'Jeiel',
|
|
32
32
|
email: 'jeiel.e2e@example.com',
|
|
33
|
-
password: '
|
|
33
|
+
password: 'strongPassword123',
|
|
34
34
|
})
|
|
35
35
|
.expect(201);
|
|
36
36
|
|
|
@@ -46,7 +46,7 @@ describe('Auth (e2e)', () => {
|
|
|
46
46
|
.post('/auth/login')
|
|
47
47
|
.send({
|
|
48
48
|
email: 'jeiel.e2e@example.com',
|
|
49
|
-
password: '
|
|
49
|
+
password: 'strongPassword123',
|
|
50
50
|
})
|
|
51
51
|
.expect(200);
|
|
52
52
|
|
|
@@ -79,13 +79,13 @@ describe('Auth (e2e)', () => {
|
|
|
79
79
|
.expect(401);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
it('
|
|
82
|
+
it('rejects registration with a duplicate email', async () => {
|
|
83
83
|
const server = app.getHttpServer();
|
|
84
84
|
|
|
85
85
|
const payload = {
|
|
86
86
|
name: 'Jeiel',
|
|
87
|
-
email: '
|
|
88
|
-
password: '
|
|
87
|
+
email: 'duplicate.e2e@example.com',
|
|
88
|
+
password: 'strongPassword123',
|
|
89
89
|
};
|
|
90
90
|
|
|
91
91
|
await request(server)
|
|
@@ -99,7 +99,7 @@ describe('Auth (e2e)', () => {
|
|
|
99
99
|
.expect(409);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
it('
|
|
102
|
+
it('rejects login with invalid credentials', async () => {
|
|
103
103
|
await request(app.getHttpServer())
|
|
104
104
|
.post('/auth/login')
|
|
105
105
|
.send({
|
|
@@ -108,4 +108,4 @@ describe('Auth (e2e)', () => {
|
|
|
108
108
|
})
|
|
109
109
|
.expect(401);
|
|
110
110
|
});
|
|
111
|
-
});
|
|
111
|
+
});
|
|
@@ -24,7 +24,7 @@ describe('Session auth (e2e)', () => {
|
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it('
|
|
27
|
+
it('registers, authenticates, and closes a CSRF-protected session', async () => {
|
|
28
28
|
const agent = request.agent(app.getHttpServer());
|
|
29
29
|
|
|
30
30
|
await agent
|
|
@@ -39,13 +39,13 @@ describe('Session auth (e2e)', () => {
|
|
|
39
39
|
|
|
40
40
|
expect(csrfToken).toMatch(/^[a-f0-9]{64}$/);
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// State-changing requests must be rejected without the token.
|
|
43
43
|
await agent
|
|
44
44
|
.post('/auth/register')
|
|
45
45
|
.send({
|
|
46
46
|
name: 'Jeiel',
|
|
47
47
|
email: 'jeiel.session@example.com',
|
|
48
|
-
password: '
|
|
48
|
+
password: 'strongPassword123',
|
|
49
49
|
})
|
|
50
50
|
.expect(403);
|
|
51
51
|
|
|
@@ -55,7 +55,7 @@ describe('Session auth (e2e)', () => {
|
|
|
55
55
|
.send({
|
|
56
56
|
name: 'Jeiel',
|
|
57
57
|
email: 'jeiel.session@example.com',
|
|
58
|
-
password: '
|
|
58
|
+
password: 'strongPassword123',
|
|
59
59
|
})
|
|
60
60
|
.expect(201);
|
|
61
61
|
|
|
@@ -93,7 +93,7 @@ describe('Session auth (e2e)', () => {
|
|
|
93
93
|
.expect(401);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
it('
|
|
96
|
+
it('logs in and refreshes the session CSRF token', async () => {
|
|
97
97
|
const registrationAgent = request.agent(
|
|
98
98
|
app.getHttpServer(),
|
|
99
99
|
);
|
|
@@ -109,9 +109,9 @@ describe('Session auth (e2e)', () => {
|
|
|
109
109
|
.post('/auth/register')
|
|
110
110
|
.set('x-csrf-token', registrationCsrfToken)
|
|
111
111
|
.send({
|
|
112
|
-
name: '
|
|
112
|
+
name: 'Session User',
|
|
113
113
|
email: 'login.session@example.com',
|
|
114
|
-
password: '
|
|
114
|
+
password: 'strongPassword123',
|
|
115
115
|
})
|
|
116
116
|
.expect(201);
|
|
117
117
|
|
|
@@ -137,7 +137,7 @@ describe('Session auth (e2e)', () => {
|
|
|
137
137
|
.set('x-csrf-token', loginCsrfToken)
|
|
138
138
|
.send({
|
|
139
139
|
email: 'login.session@example.com',
|
|
140
|
-
password: '
|
|
140
|
+
password: 'strongPassword123',
|
|
141
141
|
})
|
|
142
142
|
.expect(200);
|
|
143
143
|
|
|
@@ -156,4 +156,4 @@ describe('Session auth (e2e)', () => {
|
|
|
156
156
|
.get('/users/me')
|
|
157
157
|
.expect(200);
|
|
158
158
|
});
|
|
159
|
-
});
|
|
159
|
+
});
|
|
@@ -27,7 +27,7 @@ describe('Users (e2e)', () => {
|
|
|
27
27
|
);
|
|
28
28
|
|
|
29
29
|
const user = usersRepository.create({
|
|
30
|
-
name: '
|
|
30
|
+
name: 'Test User',
|
|
31
31
|
email,
|
|
32
32
|
passwordHash,
|
|
33
33
|
role,
|
|
@@ -66,22 +66,22 @@ describe('Users (e2e)', () => {
|
|
|
66
66
|
await app.close();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it('
|
|
69
|
+
it('rejects access without a token', async () => {
|
|
70
70
|
await request(app.getHttpServer())
|
|
71
71
|
.get('/users')
|
|
72
72
|
.expect(401);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
it('ADMIN
|
|
75
|
+
it('allows ADMIN to create, list, update, and delete a user', async () => {
|
|
76
76
|
await createUserWithRole(
|
|
77
77
|
'admin.e2e@example.com',
|
|
78
|
-
'
|
|
78
|
+
'strongPassword123',
|
|
79
79
|
Role.ADMIN,
|
|
80
80
|
);
|
|
81
81
|
|
|
82
82
|
const token = await loginAndGetToken(
|
|
83
83
|
'admin.e2e@example.com',
|
|
84
|
-
'
|
|
84
|
+
'strongPassword123',
|
|
85
85
|
);
|
|
86
86
|
|
|
87
87
|
const server = app.getHttpServer();
|
|
@@ -90,9 +90,9 @@ describe('Users (e2e)', () => {
|
|
|
90
90
|
.post('/users')
|
|
91
91
|
.set('Authorization', `Bearer ${token}`)
|
|
92
92
|
.send({
|
|
93
|
-
name: '
|
|
94
|
-
email: '
|
|
95
|
-
password: '
|
|
93
|
+
name: 'New User',
|
|
94
|
+
email: 'new.e2e@example.com',
|
|
95
|
+
password: 'strongPassword123',
|
|
96
96
|
})
|
|
97
97
|
.expect(201);
|
|
98
98
|
|
|
@@ -126,16 +126,16 @@ describe('Users (e2e)', () => {
|
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
// nestforge:feature:rbac
|
|
129
|
-
it('USER
|
|
129
|
+
it('allows USER to read but not create users', async () => {
|
|
130
130
|
await createUserWithRole(
|
|
131
131
|
'user.e2e@example.com',
|
|
132
|
-
'
|
|
132
|
+
'strongPassword123',
|
|
133
133
|
Role.USER,
|
|
134
134
|
);
|
|
135
135
|
|
|
136
136
|
const token = await loginAndGetToken(
|
|
137
137
|
'user.e2e@example.com',
|
|
138
|
-
'
|
|
138
|
+
'strongPassword123',
|
|
139
139
|
);
|
|
140
140
|
|
|
141
141
|
const server = app.getHttpServer();
|
|
@@ -149,24 +149,24 @@ describe('Users (e2e)', () => {
|
|
|
149
149
|
.post('/users')
|
|
150
150
|
.set('Authorization', `Bearer ${token}`)
|
|
151
151
|
.send({
|
|
152
|
-
name: '
|
|
153
|
-
email: '
|
|
154
|
-
password: '
|
|
152
|
+
name: 'Should Not Be Created',
|
|
153
|
+
email: 'blocked.e2e@example.com',
|
|
154
|
+
password: 'strongPassword123',
|
|
155
155
|
})
|
|
156
156
|
.expect(403);
|
|
157
157
|
});
|
|
158
158
|
// nestforge:feature:rbac:end
|
|
159
159
|
|
|
160
|
-
it('GET /users/me
|
|
160
|
+
it('returns the authenticated user from GET /users/me', async () => {
|
|
161
161
|
await createUserWithRole(
|
|
162
162
|
'me.e2e@example.com',
|
|
163
|
-
'
|
|
163
|
+
'strongPassword123',
|
|
164
164
|
Role.USER,
|
|
165
165
|
);
|
|
166
166
|
|
|
167
167
|
const token = await loginAndGetToken(
|
|
168
168
|
'me.e2e@example.com',
|
|
169
|
-
'
|
|
169
|
+
'strongPassword123',
|
|
170
170
|
);
|
|
171
171
|
|
|
172
172
|
const response = await request(
|
|
@@ -180,4 +180,4 @@ describe('Users (e2e)', () => {
|
|
|
180
180
|
'me.e2e@example.com',
|
|
181
181
|
);
|
|
182
182
|
});
|
|
183
|
-
});
|
|
183
|
+
});
|