speedrun-cli 2.6.1

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.
Files changed (159) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +620 -0
  4. package/bin/cli.js +224 -0
  5. package/index.js +12 -0
  6. package/package.json +74 -0
  7. package/src/constants.js +65 -0
  8. package/src/generator.js +271 -0
  9. package/src/index.js +13 -0
  10. package/src/moduleGenerator.js +586 -0
  11. package/src/postSetup.js +365 -0
  12. package/src/prompts.js +189 -0
  13. package/src/utils.js +112 -0
  14. package/templates/README.md +81 -0
  15. package/templates/base/eslint.config.mjs +34 -0
  16. package/templates/base/gitignore +78 -0
  17. package/templates/base/nest-cli.json +8 -0
  18. package/templates/base/src/common/constants/cookie.config.ts +20 -0
  19. package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
  20. package/templates/base/src/common/decorators/public.decorator.ts +4 -0
  21. package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
  22. package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
  23. package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
  24. package/templates/base/src/common/guards/auth.guard.ts +51 -0
  25. package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
  26. package/templates/base/src/common/guards/roles.guard.ts +45 -0
  27. package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
  28. package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
  29. package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
  30. package/templates/base/src/common/validators/password.validator.ts +37 -0
  31. package/templates/base/src/config/config.module.ts +14 -0
  32. package/templates/base/src/config/logger.config.ts +109 -0
  33. package/templates/base/src/main.ts +84 -0
  34. package/templates/base/src/modules/auth/auth.controller.ts +133 -0
  35. package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
  36. package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
  37. package/templates/base/src/modules/health/health.controller.ts +20 -0
  38. package/templates/base/src/modules/health/health.module.ts +7 -0
  39. package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
  40. package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
  41. package/templates/base/src/modules/users/users.controller.ts +65 -0
  42. package/templates/base/test/app.e2e-spec.ts +24 -0
  43. package/templates/base/test/jest-e2e.json +9 -0
  44. package/templates/base/tsconfig.build.json +4 -0
  45. package/templates/base/tsconfig.json +24 -0
  46. package/templates/base-crud/CRUD_README.md +385 -0
  47. package/templates/base-crud/src/common/base/base.controller.ts +321 -0
  48. package/templates/base-crud/src/common/base/base.service.ts +192 -0
  49. package/templates/base-crud/src/common/base/index.ts +20 -0
  50. package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
  51. package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
  52. package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
  53. package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
  54. package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
  55. package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
  56. package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
  57. package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
  58. package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
  59. package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
  60. package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
  61. package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
  62. package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
  63. package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
  64. package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
  65. package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
  66. package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
  67. package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
  68. package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
  69. package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
  70. package/templates/database/mongodb/.env.example +22 -0
  71. package/templates/database/mysql/.env.example +24 -0
  72. package/templates/database/mysql/drizzle.config.ts +13 -0
  73. package/templates/database/mysql/package.json +5 -0
  74. package/templates/database/mysql/prisma/schema.prisma +55 -0
  75. package/templates/database/mysql/src/database/drizzle.ts +13 -0
  76. package/templates/database/mysql/src/database/schema.ts +58 -0
  77. package/templates/database/postgres/.env.example +24 -0
  78. package/templates/database/postgres/drizzle.config.ts +13 -0
  79. package/templates/database/postgres/package.json +8 -0
  80. package/templates/database/postgres/prisma/schema.prisma +55 -0
  81. package/templates/database/sqlite/.env.example +24 -0
  82. package/templates/database/sqlite/drizzle.config.ts +13 -0
  83. package/templates/database/sqlite/package.json +8 -0
  84. package/templates/database/sqlite/prisma/schema.prisma +48 -0
  85. package/templates/database/sqlite/src/database/drizzle.ts +11 -0
  86. package/templates/database/sqlite/src/database/schema.ts +52 -0
  87. package/templates/orm/drizzle/drizzle.config.ts +13 -0
  88. package/templates/orm/drizzle/package.json +90 -0
  89. package/templates/orm/drizzle/src/app.module.ts +73 -0
  90. package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
  91. package/templates/orm/drizzle/src/database/database.module.ts +25 -0
  92. package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
  93. package/templates/orm/drizzle/src/database/schema.ts +60 -0
  94. package/templates/orm/drizzle/src/database/seed.ts +87 -0
  95. package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
  96. package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
  97. package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
  98. package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
  99. package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
  100. package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
  101. package/templates/orm/mongoose/.env.example +22 -0
  102. package/templates/orm/mongoose/package.json +86 -0
  103. package/templates/orm/mongoose/src/app.module.ts +73 -0
  104. package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
  105. package/templates/orm/mongoose/src/database/database.module.ts +19 -0
  106. package/templates/orm/mongoose/src/database/seed.ts +107 -0
  107. package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
  108. package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
  109. package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
  110. package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
  111. package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
  112. package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
  113. package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
  114. package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
  115. package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
  116. package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
  117. package/templates/orm/mongoose/src/schemas/index.ts +2 -0
  118. package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
  119. package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
  120. package/templates/orm/prisma/package.json +102 -0
  121. package/templates/orm/prisma/prisma/schema.prisma +60 -0
  122. package/templates/orm/prisma/prisma/seed.ts +69 -0
  123. package/templates/orm/prisma/src/app.module.ts +55 -0
  124. package/templates/orm/prisma/src/config/env.validation.ts +56 -0
  125. package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
  126. package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
  127. package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
  128. package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
  129. package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
  130. package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
  131. package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
  132. package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
  133. package/templates/orm/typeorm/package.json +100 -0
  134. package/templates/orm/typeorm/src/app.module.ts +55 -0
  135. package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
  136. package/templates/orm/typeorm/src/database/data-source.ts +19 -0
  137. package/templates/orm/typeorm/src/database/database.module.ts +27 -0
  138. package/templates/orm/typeorm/src/database/seed.ts +76 -0
  139. package/templates/orm/typeorm/src/entities/index.ts +2 -0
  140. package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
  141. package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
  142. package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
  143. package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
  144. package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
  145. package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
  146. package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
  147. package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
  148. package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
  149. package/templates/swagger/src/main.ts +116 -0
  150. package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
  151. package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
  152. package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
  153. package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
  154. package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
  155. package/templates/swagger/src/modules/users/users.controller.ts +214 -0
  156. package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
  157. package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
  158. package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
  159. package/templates/swagger-mongoose/src/modules/users/dtos/update-user.dto.ts +23 -0
@@ -0,0 +1,308 @@
1
+ import {
2
+ Injectable,
3
+ ConflictException,
4
+ ForbiddenException,
5
+ UnauthorizedException,
6
+ NotFoundException,
7
+ } from '@nestjs/common';
8
+
9
+ import { PrismaService } from 'src/prisma/prisma.service';
10
+ import * as bcrypt from 'bcrypt';
11
+ import { JwtService } from '@nestjs/jwt';
12
+ import { ConfigService } from '@nestjs/config';
13
+ import { User } from '@prisma/client';
14
+ import { Logger } from 'nestjs-pino';
15
+ import { SignupDto } from './dtos/signup.dto';
16
+ import { LoginDto } from './dtos/login.dto';
17
+
18
+ @Injectable()
19
+ export class AuthService {
20
+ constructor(
21
+ private prisma: PrismaService,
22
+ private jwtService: JwtService,
23
+ private config: ConfigService,
24
+ private logger: Logger,
25
+ ) {}
26
+
27
+ async signup(signupDto: SignupDto) {
28
+ const { email, password, fullName } = signupDto;
29
+
30
+ const existingUser = await this.prisma.user.findUnique({
31
+ where: { email },
32
+ });
33
+
34
+ if (existingUser) {
35
+ throw new ConflictException('Email already in use');
36
+ }
37
+
38
+ const hashedPassword = await this.hashData(password);
39
+
40
+ const newUser = await this.prisma.user.create({
41
+ data: {
42
+ email,
43
+ passwordHash: hashedPassword,
44
+ fullName,
45
+ },
46
+ });
47
+
48
+ // Log without PII (email address)
49
+ this.logger.log({
50
+ message: 'New user registered',
51
+ userId: newUser.id,
52
+ role: newUser.role,
53
+ });
54
+
55
+ return {
56
+ user: {
57
+ id: newUser.id,
58
+ email: newUser.email,
59
+ fullName: newUser.fullName,
60
+ role: newUser.role,
61
+ },
62
+ message: 'User registered successfully',
63
+ };
64
+ }
65
+
66
+ async login(loginDto: LoginDto, deviceInfo?: string, ipAddress?: string) {
67
+ const { email, password } = loginDto;
68
+
69
+ const user = await this.prisma.user.findUnique({
70
+ where: { email },
71
+ });
72
+
73
+ // Prevent timing attacks by always hashing, even if user doesn't exist
74
+ const passwordHash =
75
+ user?.passwordHash ||
76
+ (await this.hashData('dummy-password-to-prevent-timing-attack'));
77
+ const passwordMatches = await bcrypt.compare(password, passwordHash);
78
+
79
+ // Use consistent error message to prevent account enumeration
80
+ if (!user || !user.isActive || !passwordMatches) {
81
+ throw new UnauthorizedException('Invalid email or password');
82
+ }
83
+
84
+ const tokens = await this.generateTokens(user);
85
+ const hashedRt = await this.hashData(tokens.refreshToken);
86
+
87
+ // Store refresh token with device info for multi-device support
88
+ const refreshExpiry =
89
+ this.config.get<string>('JWT_REFRESH_EXPIRY') || '30d';
90
+ const expiryMs = this.parseExpiryToMilliseconds(refreshExpiry);
91
+ const expiresAt = new Date(Date.now() + expiryMs);
92
+
93
+ await this.prisma.refreshToken.create({
94
+ data: {
95
+ token: hashedRt,
96
+ userId: user.id,
97
+ deviceInfo: deviceInfo || 'Unknown Device',
98
+ ipAddress: ipAddress || 'Unknown IP',
99
+ expiresAt,
100
+ },
101
+ });
102
+
103
+ // Clean up expired tokens for this user
104
+ await this.cleanupExpiredTokens(user.id);
105
+
106
+ this.logger.log({
107
+ message: 'User logged in',
108
+ userId: user.id,
109
+ role: user.role,
110
+ timestamp: new Date().toISOString(),
111
+ });
112
+
113
+ return {
114
+ user: {
115
+ id: user.id,
116
+ email: user.email,
117
+ fullName: user.fullName,
118
+ role: user.role,
119
+ },
120
+ ...tokens,
121
+ };
122
+ }
123
+
124
+ async refreshToken(
125
+ userId: string,
126
+ rt: string,
127
+ deviceInfo?: string,
128
+ ipAddress?: string,
129
+ ) {
130
+ const user = await this.prisma.user.findUnique({
131
+ where: { id: userId },
132
+ });
133
+
134
+ if (!user || !user.isActive) {
135
+ throw new ForbiddenException('Invalid refresh token');
136
+ }
137
+
138
+ // Find matching refresh token in database
139
+ const storedTokens = await this.prisma.refreshToken.findMany({
140
+ where: {
141
+ userId: user.id,
142
+ expiresAt: { gte: new Date() },
143
+ },
144
+ });
145
+
146
+ let isValidToken = false;
147
+ let validTokenId: string | null = null;
148
+
149
+ // Check if provided token matches any stored token
150
+ for (const storedToken of storedTokens) {
151
+ const matches = await bcrypt.compare(rt, storedToken.token);
152
+ if (matches) {
153
+ isValidToken = true;
154
+ validTokenId = storedToken.id;
155
+ break;
156
+ }
157
+ }
158
+
159
+ if (!isValidToken || !validTokenId) {
160
+ throw new UnauthorizedException('Invalid refresh token');
161
+ }
162
+
163
+ // Generate new tokens
164
+ const tokens = await this.generateTokens(user);
165
+ const hashedRt = await this.hashData(tokens.refreshToken);
166
+
167
+ // Update the refresh token (rotation)
168
+ const refreshExpiry =
169
+ this.config.get<string>('JWT_REFRESH_EXPIRY') || '30d';
170
+ const expiryMs = this.parseExpiryToMilliseconds(refreshExpiry);
171
+ const expiresAt = new Date(Date.now() + expiryMs);
172
+
173
+ await this.prisma.refreshToken.update({
174
+ where: { id: validTokenId },
175
+ data: {
176
+ token: hashedRt,
177
+ deviceInfo: deviceInfo || 'Unknown Device',
178
+ ipAddress: ipAddress || 'Unknown IP',
179
+ expiresAt,
180
+ updatedAt: new Date(),
181
+ },
182
+ });
183
+
184
+ return tokens;
185
+ }
186
+
187
+ async logout(userId: string, rt?: string) {
188
+ if (rt) {
189
+ // Find and delete the specific refresh token
190
+ const storedTokens = await this.prisma.refreshToken.findMany({
191
+ where: { userId },
192
+ });
193
+
194
+ for (const storedToken of storedTokens) {
195
+ const matches = await bcrypt.compare(rt, storedToken.token);
196
+ if (matches) {
197
+ await this.prisma.refreshToken.delete({
198
+ where: { id: storedToken.id },
199
+ });
200
+ break;
201
+ }
202
+ }
203
+ } else {
204
+ // Logout from all devices
205
+ await this.prisma.refreshToken.deleteMany({
206
+ where: { userId },
207
+ });
208
+ }
209
+
210
+ return {
211
+ message: 'Logged out successfully',
212
+ };
213
+ }
214
+
215
+ async getMe(userId: string) {
216
+ const user = await this.prisma.user.findUnique({
217
+ where: { id: userId },
218
+ });
219
+
220
+ if (!user) {
221
+ throw new NotFoundException('User not found');
222
+ }
223
+
224
+ const safeUser = {
225
+ id: user.id,
226
+ email: user.email,
227
+ fullName: user.fullName,
228
+ role: user.role,
229
+ };
230
+
231
+ return safeUser;
232
+ }
233
+
234
+ // Helper Methods
235
+
236
+ async hashData(data: string): Promise<string> {
237
+ const salt = await bcrypt.genSalt(12); // Use 12 rounds for 2025 security standards
238
+ return bcrypt.hash(data, salt);
239
+ }
240
+
241
+ async generateTokens(
242
+ user: User,
243
+ ): Promise<{ accessToken: string; refreshToken: string }> {
244
+ const payload = { sub: user.id, role: user.role, email: user.email };
245
+ const accessExpiry = this.config.get<string>('JWT_ACCESS_EXPIRY') || '15m';
246
+ const refreshExpiry = this.config.get<string>('JWT_REFRESH_EXPIRY') || '7d';
247
+
248
+ const [accessToken, refreshToken] = await Promise.all([
249
+ // @ts-expect-error - JWT library type definition issue with expiresIn accepting string
250
+ this.jwtService.signAsync(payload, {
251
+ secret: this.config.get<string>('JWT_ACCESS_SECRET'),
252
+ expiresIn: accessExpiry,
253
+ }),
254
+ // @ts-expect-error - JWT library type definition issue with expiresIn accepting string
255
+ this.jwtService.signAsync(payload, {
256
+ secret: this.config.get<string>('JWT_REFRESH_SECRET'),
257
+ expiresIn: refreshExpiry,
258
+ }),
259
+ ]);
260
+ return {
261
+ accessToken,
262
+ refreshToken,
263
+ };
264
+ }
265
+
266
+ private async cleanupExpiredTokens(userId: string) {
267
+ // Remove expired refresh tokens for this user
268
+ await this.prisma.refreshToken.deleteMany({
269
+ where: {
270
+ userId,
271
+ expiresAt: { lt: new Date() },
272
+ },
273
+ });
274
+
275
+ // Optional: Limit to 5 most recent tokens per user
276
+ const tokens = await this.prisma.refreshToken.findMany({
277
+ where: { userId },
278
+ orderBy: { createdAt: 'desc' },
279
+ skip: 5,
280
+ take: 100,
281
+ });
282
+
283
+ if (tokens.length > 0) {
284
+ await this.prisma.refreshToken.deleteMany({
285
+ where: {
286
+ id: { in: tokens.map((t) => t.id) },
287
+ },
288
+ });
289
+ }
290
+ }
291
+
292
+ private parseExpiryToMilliseconds(expiry: string): number {
293
+ const match = expiry.match(/^(\d+)([smhd])$/);
294
+ if (!match) return 30 * 24 * 60 * 60 * 1000; // default 30 days
295
+
296
+ const value = parseInt(match[1], 10);
297
+ const unit = match[2];
298
+
299
+ const units: { [key: string]: number } = {
300
+ s: 1000,
301
+ m: 60 * 1000,
302
+ h: 60 * 60 * 1000,
303
+ d: 24 * 60 * 60 * 1000,
304
+ };
305
+
306
+ return value * units[unit];
307
+ }
308
+ }
@@ -0,0 +1,26 @@
1
+ import { Controller, Get } from '@nestjs/common';
2
+ import {
3
+ HealthCheckService,
4
+ HealthCheck,
5
+ PrismaHealthIndicator,
6
+ } from '@nestjs/terminus';
7
+ import { PrismaService } from 'src/prisma/prisma.service';
8
+ import { Public } from 'src/common/decorators/public.decorator';
9
+
10
+ @Controller('health')
11
+ export class HealthController {
12
+ constructor(
13
+ private health: HealthCheckService,
14
+ private prismaHealth: PrismaHealthIndicator,
15
+ private prisma: PrismaService,
16
+ ) {}
17
+
18
+ @Get()
19
+ @Public()
20
+ @HealthCheck()
21
+ check() {
22
+ return this.health.check([
23
+ () => this.prismaHealth.pingCheck('database', this.prisma),
24
+ ]);
25
+ }
26
+ }
@@ -0,0 +1,10 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { TerminusModule } from '@nestjs/terminus';
3
+ import { HealthController } from './health.controller';
4
+ import { PrismaModule } from 'src/prisma/prisma.module';
5
+
6
+ @Module({
7
+ imports: [TerminusModule, PrismaModule],
8
+ controllers: [HealthController],
9
+ })
10
+ export class HealthModule {}
@@ -0,0 +1,11 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { UsersService } from './users.service';
3
+ import { UsersController } from './users.controller';
4
+ import { PrismaModule } from 'src/prisma/prisma.module';
5
+
6
+ @Module({
7
+ imports: [PrismaModule],
8
+ controllers: [UsersController],
9
+ providers: [UsersService],
10
+ })
11
+ export class UsersModule {}
@@ -0,0 +1,105 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { PrismaService } from 'src/prisma/prisma.service';
3
+ import { UpdateProfileDto } from './dtos/update-profile.dto';
4
+ import { User } from '@prisma/client';
5
+ import { UpdateUserDto } from './dtos/update-user.dto';
6
+
7
+ @Injectable()
8
+ export class UsersService {
9
+ constructor(private readonly prisma: PrismaService) {}
10
+
11
+ async getProfile(userId: string) {
12
+ const user = await this.prisma.user.findFirst({
13
+ where: { id: userId },
14
+ });
15
+
16
+ return this.sanitizeUser(user);
17
+ }
18
+
19
+ async updateProfile(userId: string, updateProfileDto: UpdateProfileDto) {
20
+ const updatedUser = await this.prisma.user.update({
21
+ where: { id: userId },
22
+ data: { ...updateProfileDto },
23
+ });
24
+ return this.sanitizeUser(updatedUser);
25
+ }
26
+
27
+ // admin methods
28
+ async getAllUsers(page: number = 1, limit: number = 10) {
29
+ const skip = (page - 1) * limit;
30
+
31
+ const [users, total] = await Promise.all([
32
+ this.prisma.user.findMany({
33
+ where: { isActive: true },
34
+ skip,
35
+ take: limit,
36
+ orderBy: { createdAt: 'desc' },
37
+ }),
38
+ this.prisma.user.count({ where: { isActive: true } }),
39
+ ]);
40
+
41
+ const sanitizedUsers = users.map((user) => this.sanitizeUser(user));
42
+ const totalPages = Math.ceil(total / limit);
43
+
44
+ return {
45
+ data: sanitizedUsers,
46
+ meta: {
47
+ total,
48
+ page,
49
+ limit,
50
+ totalPages,
51
+ hasNext: page < totalPages,
52
+ hasPrevious: page > 1,
53
+ },
54
+ };
55
+ }
56
+
57
+ async getUserById(userId: string) {
58
+ if (!userId) {
59
+ return null;
60
+ }
61
+ const user = await this.prisma.user.findUnique({
62
+ where: { id: userId },
63
+ });
64
+ if (!user) {
65
+ return null;
66
+ }
67
+ return this.sanitizeUser(user);
68
+ }
69
+
70
+ async updateUserById(userId: string, updateUserDto: UpdateUserDto) {
71
+ const updatedUser = await this.prisma.user.update({
72
+ where: { id: userId },
73
+ data: { ...updateUserDto },
74
+ });
75
+
76
+ return this.sanitizeUser(updatedUser);
77
+ }
78
+
79
+ async deleteUserById(userId: string) {
80
+ // Soft delete: set isActive to false instead of hard delete
81
+ await this.prisma.user.update({
82
+ where: { id: userId },
83
+ data: { isActive: false },
84
+ });
85
+
86
+ // Also invalidate all refresh tokens for this user
87
+ await this.prisma.refreshToken.deleteMany({
88
+ where: { userId },
89
+ });
90
+
91
+ return { message: 'User deleted successfully' };
92
+ }
93
+
94
+ // utils
95
+
96
+ sanitizeUser(user: User | null) {
97
+ if (!user) {
98
+ return null;
99
+ }
100
+
101
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
102
+ const { refreshToken, passwordHash, ...safeUser } = user;
103
+ return safeUser;
104
+ }
105
+ }
@@ -0,0 +1,9 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { PrismaService } from './prisma.service';
3
+
4
+ @Module({
5
+ exports: [PrismaService],
6
+ controllers: [],
7
+ providers: [PrismaService],
8
+ })
9
+ export class PrismaModule {}
@@ -0,0 +1,16 @@
1
+ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ @Injectable()
5
+ export class PrismaService
6
+ extends PrismaClient
7
+ implements OnModuleInit, OnModuleDestroy
8
+ {
9
+ async onModuleInit() {
10
+ await this.$connect();
11
+ }
12
+
13
+ async onModuleDestroy() {
14
+ await this.$disconnect();
15
+ }
16
+ }
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "nestjs-jwt-rbac-boilerplate",
3
+ "version": "0.0.1",
4
+ "description": "Production-ready NestJS authentication system with JWT, refresh tokens, and RBAC",
5
+ "author": "",
6
+ "private": true,
7
+ "license": "MIT",
8
+ "scripts": {
9
+ "build": "nest build",
10
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11
+ "start": "nest start",
12
+ "start:dev": "nest start --watch",
13
+ "start:debug": "nest start --debug --watch",
14
+ "start:prod": "node dist/main",
15
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16
+ "test": "jest",
17
+ "test:watch": "jest --watch",
18
+ "test:cov": "jest --coverage",
19
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20
+ "test:e2e": "jest --config ./test/jest-e2e.json",
21
+ "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
22
+ "migration:generate": "npm run typeorm -- migration:generate -d src/database/data-source.ts",
23
+ "migration:run": "npm run typeorm -- migration:run -d src/database/data-source.ts",
24
+ "migration:revert": "npm run typeorm -- migration:revert -d src/database/data-source.ts",
25
+ "schema:sync": "npm run typeorm -- schema:sync -d src/database/data-source.ts",
26
+ "seed": "ts-node src/database/seed.ts"
27
+ },
28
+ "dependencies": {
29
+ "@nestjs/common": "^11.0.1",
30
+ "@nestjs/config": "^4.0.2",
31
+ "@nestjs/core": "^11.0.1",
32
+ "@nestjs/jwt": "^11.0.1",
33
+ "@nestjs/mapped-types": "*",
34
+ "@nestjs/platform-express": "^11.0.1",
35
+ "@nestjs/schedule": "^6.0.1",
36
+ "@nestjs/terminus": "^11.0.0",
37
+ "@nestjs/throttler": "^6.4.0",
38
+ "@nestjs/typeorm": "^11.0.0",
39
+ "typeorm": "^0.3.20",
40
+ "pg": "^8.11.3",
41
+ "mysql2": "^3.9.1",
42
+ "better-sqlite3": "^9.4.3",
43
+ "bcrypt": "^6.0.0",
44
+ "class-transformer": "^0.5.1",
45
+ "class-validator": "^0.14.2",
46
+ "cookie-parser": "^1.4.7",
47
+ "dotenv": "^17.2.3",
48
+ "helmet": "^8.1.0",
49
+ "nestjs-pino": "^4.4.1",
50
+ "pino-http": "^10.5.0",
51
+ "pino-pretty": "^13.1.2",
52
+ "reflect-metadata": "^0.2.2",
53
+ "rxjs": "^7.8.1",
54
+ "zod": "^4.1.12"
55
+ },
56
+ "devDependencies": {
57
+ "@eslint/eslintrc": "^3.2.0",
58
+ "@eslint/js": "^9.18.0",
59
+ "@nestjs/cli": "^11.0.0",
60
+ "@nestjs/schematics": "^11.0.0",
61
+ "@nestjs/testing": "^11.0.1",
62
+ "@types/bcrypt": "^6.0.0",
63
+ "@types/cookie-parser": "^1.4.10",
64
+ "@types/express": "^5.0.0",
65
+ "@types/jest": "^30.0.0",
66
+ "@types/node": "^22.10.7",
67
+ "@types/supertest": "^6.0.2",
68
+ "eslint": "^9.18.0",
69
+ "eslint-config-prettier": "^10.0.1",
70
+ "eslint-plugin-prettier": "^5.2.2",
71
+ "globals": "^16.0.0",
72
+ "jest": "^30.0.0",
73
+ "prettier": "^3.4.2",
74
+ "source-map-support": "^0.5.21",
75
+ "supertest": "^7.0.0",
76
+ "ts-jest": "^29.2.5",
77
+ "ts-loader": "^9.5.2",
78
+ "ts-node": "^10.9.2",
79
+ "tsconfig-paths": "^4.2.0",
80
+ "typescript": "^5.7.3",
81
+ "typescript-eslint": "^8.20.0"
82
+ },
83
+ "jest": {
84
+ "moduleFileExtensions": [
85
+ "js",
86
+ "json",
87
+ "ts"
88
+ ],
89
+ "rootDir": "src",
90
+ "testRegex": ".*\\.spec\\.ts$",
91
+ "transform": {
92
+ "^.+\\.(t|j)s$": "ts-jest"
93
+ },
94
+ "collectCoverageFrom": [
95
+ "**/*.(t|j)s"
96
+ ],
97
+ "coverageDirectory": "../coverage",
98
+ "testEnvironment": "node"
99
+ }
100
+ }
@@ -0,0 +1,55 @@
1
+ import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
2
+ import { AuthModule } from './modules/auth/auth.module';
3
+ import { LoggerModule } from 'nestjs-pino';
4
+ import { DatabaseModule } from './database/database.module';
5
+ import { UsersModule } from './modules/users/users.module';
6
+ import { HealthModule } from './modules/health/health.module';
7
+ import { AppConfigModule } from './config/config.module';
8
+ import { loggerConfig } from './config/logger.config';
9
+ import { CorrelationIdMiddleware } from './common/middleware/correlation-id.middleware';
10
+ import { RolesGuard } from './common/guards/roles.guard';
11
+ import { AuthGuard } from './common/guards/auth.guard';
12
+ import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
13
+
14
+ @Module({
15
+ imports: [
16
+ LoggerModule.forRoot(loggerConfig),
17
+ ThrottlerModule.forRoot([
18
+ {
19
+ name: 'default',
20
+ ttl: 60000,
21
+ limit: 10,
22
+ },
23
+ {
24
+ name: 'strict',
25
+ ttl: 60000,
26
+ limit: 5,
27
+ },
28
+ ]),
29
+ AppConfigModule,
30
+ DatabaseModule,
31
+ AuthModule,
32
+ UsersModule,
33
+ HealthModule,
34
+ ],
35
+ controllers: [],
36
+ providers: [
37
+ {
38
+ provide: 'APP_GUARD',
39
+ useClass: ThrottlerGuard,
40
+ },
41
+ {
42
+ provide: 'APP_GUARD',
43
+ useClass: AuthGuard,
44
+ },
45
+ {
46
+ provide: 'APP_GUARD',
47
+ useClass: RolesGuard,
48
+ },
49
+ ],
50
+ })
51
+ export class AppModule implements NestModule {
52
+ configure(consumer: MiddlewareConsumer) {
53
+ consumer.apply(CorrelationIdMiddleware).forRoutes('*');
54
+ }
55
+ }