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,298 @@
1
+ import {
2
+ Injectable,
3
+ UnauthorizedException,
4
+ ConflictException,
5
+ ForbiddenException,
6
+ NotFoundException,
7
+ Inject,
8
+ } from '@nestjs/common';
9
+ import { ConfigService } from '@nestjs/config';
10
+ import { JwtService } from '@nestjs/jwt';
11
+ import { eq, and } from 'drizzle-orm';
12
+ import * as bcrypt from 'bcrypt';
13
+ import { DRIZZLE } from '../../database/database.module';
14
+ import { DrizzleDB } from '../../database/drizzle';
15
+ import { users, refreshTokens, User, RefreshToken } from '../../database/schema';
16
+ import { SignupDto } from './dtos/signup.dto';
17
+ import { LoginDto } from './dtos/login.dto';
18
+
19
+ const SALT_ROUNDS = 12;
20
+
21
+ @Injectable()
22
+ export class AuthService {
23
+ constructor(
24
+ @Inject(DRIZZLE) private db: DrizzleDB,
25
+ private jwtService: JwtService,
26
+ private configService: ConfigService,
27
+ ) {}
28
+
29
+ async signup(signupDto: SignupDto) {
30
+ const { fullName, email, password } = signupDto;
31
+
32
+ // Check if user already exists
33
+ const existingUser = await this.db.query.users.findFirst({
34
+ where: eq(users.email, email.toLowerCase()),
35
+ });
36
+
37
+ if (existingUser) {
38
+ throw new ConflictException('User with this email already exists');
39
+ }
40
+
41
+ // Hash password and create user
42
+ const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
43
+
44
+ const [user] = await this.db
45
+ .insert(users)
46
+ .values({
47
+ fullName,
48
+ email: email.toLowerCase(),
49
+ passwordHash,
50
+ role: 'USER',
51
+ isActive: true,
52
+ })
53
+ .returning();
54
+
55
+ return {
56
+ id: user.id,
57
+ fullName: user.fullName,
58
+ email: user.email,
59
+ role: user.role,
60
+ isActive: user.isActive,
61
+ };
62
+ }
63
+
64
+ async login(loginDto: LoginDto, deviceInfo?: string, ipAddress?: string) {
65
+ const { email, password } = loginDto;
66
+
67
+ // Find user
68
+ const user = await this.db.query.users.findFirst({
69
+ where: eq(users.email, email.toLowerCase()),
70
+ });
71
+
72
+ if (!user) {
73
+ throw new UnauthorizedException('Invalid email or password');
74
+ }
75
+
76
+ // Check if user is active
77
+ if (!user.isActive) {
78
+ throw new UnauthorizedException('Account is deactivated');
79
+ }
80
+
81
+ // Verify password
82
+ const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
83
+ if (!isPasswordValid) {
84
+ throw new UnauthorizedException('Invalid email or password');
85
+ }
86
+
87
+ // Generate tokens
88
+ const tokens = await this.generateTokens(user);
89
+
90
+ // Store refresh token in database
91
+ await this.storeRefreshToken(
92
+ tokens.refreshToken,
93
+ user.id,
94
+ deviceInfo || 'Unknown Device',
95
+ ipAddress || 'Unknown IP',
96
+ );
97
+
98
+ // Clean up expired tokens for this user
99
+ await this.cleanupExpiredTokens(user.id);
100
+
101
+ return {
102
+ user: {
103
+ id: user.id,
104
+ fullName: user.fullName,
105
+ email: user.email,
106
+ role: user.role,
107
+ isActive: user.isActive,
108
+ },
109
+ ...tokens,
110
+ };
111
+ }
112
+
113
+ async logout(userId: string, refreshToken?: string) {
114
+ if (refreshToken) {
115
+ // Find and revoke the specific refresh token by comparing hashes
116
+ const storedTokens = await this.db.query.refreshTokens.findMany({
117
+ where: eq(refreshTokens.userId, userId),
118
+ });
119
+
120
+ for (const storedToken of storedTokens) {
121
+ const matches = await bcrypt.compare(refreshToken, storedToken.token);
122
+ if (matches) {
123
+ await this.db
124
+ .update(refreshTokens)
125
+ .set({ isRevoked: true, updatedAt: new Date() })
126
+ .where(eq(refreshTokens.id, storedToken.id));
127
+ break;
128
+ }
129
+ }
130
+ } else {
131
+ // Logout from all devices - revoke all refresh tokens for this user
132
+ await this.db
133
+ .update(refreshTokens)
134
+ .set({ isRevoked: true, updatedAt: new Date() })
135
+ .where(and(eq(refreshTokens.userId, userId), eq(refreshTokens.isRevoked, false)));
136
+ }
137
+
138
+ return { message: 'Logged out successfully' };
139
+ }
140
+
141
+ async refreshToken(
142
+ userId: string,
143
+ rt: string,
144
+ deviceInfo?: string,
145
+ ipAddress?: string,
146
+ ) {
147
+ // Find the user
148
+ const user = await this.db.query.users.findFirst({
149
+ where: eq(users.id, userId),
150
+ });
151
+
152
+ if (!user) {
153
+ throw new ForbiddenException('Invalid refresh token');
154
+ }
155
+
156
+ // Find matching refresh token in database by comparing hashes
157
+ const storedTokens = await this.db.query.refreshTokens.findMany({
158
+ where: and(
159
+ eq(refreshTokens.userId, userId),
160
+ eq(refreshTokens.isRevoked, false),
161
+ ),
162
+ });
163
+
164
+ let validToken: RefreshToken | null = null;
165
+ for (const storedToken of storedTokens) {
166
+ if (new Date() < storedToken.expiresAt) {
167
+ const matches = await bcrypt.compare(rt, storedToken.token);
168
+ if (matches) {
169
+ validToken = storedToken;
170
+ break;
171
+ }
172
+ }
173
+ }
174
+
175
+ if (!validToken) {
176
+ throw new UnauthorizedException('Invalid refresh token');
177
+ }
178
+
179
+ // Revoke old refresh token (token rotation)
180
+ await this.db
181
+ .update(refreshTokens)
182
+ .set({ isRevoked: true, updatedAt: new Date() })
183
+ .where(eq(refreshTokens.id, validToken.id));
184
+
185
+ // Generate new tokens
186
+ const tokens = await this.generateTokens(user);
187
+
188
+ // Store new refresh token
189
+ await this.storeRefreshToken(
190
+ tokens.refreshToken,
191
+ user.id,
192
+ deviceInfo || 'Unknown Device',
193
+ ipAddress || 'Unknown IP',
194
+ );
195
+
196
+ return tokens;
197
+ }
198
+
199
+ async getMe(userId: string) {
200
+ const user = await this.db.query.users.findFirst({
201
+ where: eq(users.id, userId),
202
+ });
203
+
204
+ if (!user) {
205
+ throw new NotFoundException('User not found');
206
+ }
207
+
208
+ return {
209
+ id: user.id,
210
+ fullName: user.fullName,
211
+ email: user.email,
212
+ role: user.role,
213
+ isActive: user.isActive,
214
+ };
215
+ }
216
+
217
+ // Helper Methods
218
+
219
+ async hashData(data: string): Promise<string> {
220
+ return bcrypt.hash(data, SALT_ROUNDS);
221
+ }
222
+
223
+ async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> {
224
+ const payload = {
225
+ sub: user.id,
226
+ email: user.email,
227
+ role: user.role,
228
+ };
229
+
230
+ const [accessToken, refreshToken] = await Promise.all([
231
+ // @ts-expect-error - JWT library type definition issue with expiresIn accepting string
232
+ this.jwtService.signAsync(payload, {
233
+ secret: this.configService.get<string>('JWT_ACCESS_SECRET'),
234
+ expiresIn: this.configService.get<string>('JWT_ACCESS_EXPIRY') || '15m',
235
+ }),
236
+ // @ts-expect-error - JWT library type definition issue with expiresIn accepting string
237
+ this.jwtService.signAsync(payload, {
238
+ secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
239
+ expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRY') || '7d',
240
+ }),
241
+ ]);
242
+
243
+ return { accessToken, refreshToken };
244
+ }
245
+
246
+ private async storeRefreshToken(
247
+ token: string,
248
+ userId: string,
249
+ userAgent: string,
250
+ ipAddress: string,
251
+ ) {
252
+ const expiresIn = this.configService.get<string>('JWT_REFRESH_EXPIRY') || '7d';
253
+ const expiresAt = this.calculateExpiry(expiresIn);
254
+
255
+ // Hash the refresh token before storing
256
+ const hashedToken = await this.hashData(token);
257
+
258
+ await this.db.insert(refreshTokens).values({
259
+ token: hashedToken,
260
+ userId,
261
+ userAgent,
262
+ ipAddress,
263
+ expiresAt,
264
+ });
265
+ }
266
+
267
+ private calculateExpiry(expiresIn: string): Date {
268
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
269
+ if (!match) {
270
+ return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // Default 7 days
271
+ }
272
+
273
+ const value = parseInt(match[1], 10);
274
+ const unit = match[2];
275
+
276
+ const multipliers: Record<string, number> = {
277
+ s: 1000,
278
+ m: 60 * 1000,
279
+ h: 60 * 60 * 1000,
280
+ d: 24 * 60 * 60 * 1000,
281
+ };
282
+
283
+ return new Date(Date.now() + value * multipliers[unit]);
284
+ }
285
+
286
+ private async cleanupExpiredTokens(userId: string) {
287
+ // Remove expired/revoked refresh tokens for this user
288
+ const expiredTokens = await this.db.query.refreshTokens.findMany({
289
+ where: eq(refreshTokens.userId, userId),
290
+ });
291
+
292
+ for (const token of expiredTokens) {
293
+ if (token.isRevoked || new Date() > token.expiresAt) {
294
+ await this.db.delete(refreshTokens).where(eq(refreshTokens.id, token.id));
295
+ }
296
+ }
297
+ }
298
+ }
@@ -0,0 +1,34 @@
1
+ import { Controller, Get, Inject } from '@nestjs/common';
2
+ import { Pool } from 'pg';
3
+ import { DRIZZLE } from '../../database/database.module';
4
+ import { Public } from '../../common/decorators/public.decorator';
5
+
6
+ @Controller('health')
7
+ export class HealthController {
8
+ constructor(@Inject(DRIZZLE) private db: any) {}
9
+
10
+ @Public()
11
+ @Get()
12
+ async check() {
13
+ let dbStatus = 'unhealthy';
14
+
15
+ try {
16
+ // Try to execute a simple query
17
+ await this.db.execute('SELECT 1');
18
+ dbStatus = 'healthy';
19
+ } catch (error) {
20
+ dbStatus = 'unhealthy';
21
+ }
22
+
23
+ return {
24
+ status: dbStatus === 'healthy' ? 'ok' : 'error',
25
+ timestamp: new Date().toISOString(),
26
+ services: {
27
+ database: {
28
+ status: dbStatus,
29
+ type: 'postgresql',
30
+ },
31
+ },
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,7 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { HealthController } from './health.controller';
3
+
4
+ @Module({
5
+ controllers: [HealthController],
6
+ })
7
+ export class HealthModule {}
@@ -0,0 +1,10 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { UsersController } from './users.controller';
3
+ import { UsersService } from './users.service';
4
+
5
+ @Module({
6
+ controllers: [UsersController],
7
+ providers: [UsersService],
8
+ exports: [UsersService],
9
+ })
10
+ export class UsersModule {}
@@ -0,0 +1,152 @@
1
+ import {
2
+ Injectable,
3
+ NotFoundException,
4
+ ConflictException,
5
+ Inject,
6
+ } from '@nestjs/common';
7
+ import { eq, ne, and, desc, count } from 'drizzle-orm';
8
+ import * as bcrypt from 'bcrypt';
9
+ import { DRIZZLE } from '../../database/database.module';
10
+ import { DrizzleDB } from '../../database/drizzle';
11
+ import { users, refreshTokens, UserRole } from '../../database/schema';
12
+ import { UpdateProfileDto } from './dtos/update-profile.dto';
13
+ import { UpdateUserDto } from './dtos/update-user.dto';
14
+
15
+ const SALT_ROUNDS = 12;
16
+
17
+ @Injectable()
18
+ export class UsersService {
19
+ constructor(@Inject(DRIZZLE) private db: DrizzleDB) {}
20
+
21
+ async getProfile(userId: string) {
22
+ const user = await this.db.query.users.findFirst({
23
+ where: eq(users.id, userId),
24
+ });
25
+
26
+ if (!user) {
27
+ throw new NotFoundException('User not found');
28
+ }
29
+
30
+ return this.sanitizeUser(user);
31
+ }
32
+
33
+ async updateProfile(userId: string, updateProfileDto: UpdateProfileDto) {
34
+ const user = await this.db.query.users.findFirst({
35
+ where: eq(users.id, userId),
36
+ });
37
+
38
+ if (!user) {
39
+ throw new NotFoundException('User not found');
40
+ }
41
+
42
+ const updateData: Record<string, any> = { updatedAt: new Date() };
43
+ if (updateProfileDto.fullName) updateData.fullName = updateProfileDto.fullName;
44
+
45
+ const [updatedUser] = await this.db
46
+ .update(users)
47
+ .set(updateData)
48
+ .where(eq(users.id, userId))
49
+ .returning();
50
+
51
+ return this.sanitizeUser(updatedUser);
52
+ }
53
+
54
+ // Admin methods
55
+
56
+ async getAllUsers(page: number = 1, limit: number = 10) {
57
+ const offset = (page - 1) * limit;
58
+
59
+ const [userList, totalResult] = await Promise.all([
60
+ this.db.query.users.findMany({
61
+ offset,
62
+ limit,
63
+ orderBy: [desc(users.createdAt)],
64
+ }),
65
+ this.db.select({ count: count() }).from(users),
66
+ ]);
67
+
68
+ const total = totalResult[0].count;
69
+ const totalPages = Math.ceil(total / limit);
70
+
71
+ return {
72
+ data: userList.map((user) => this.sanitizeUser(user)),
73
+ meta: {
74
+ total,
75
+ page,
76
+ limit,
77
+ totalPages,
78
+ hasNext: page < totalPages,
79
+ hasPrevious: page > 1,
80
+ },
81
+ };
82
+ }
83
+
84
+ async getUserById(userId: string) {
85
+ if (!userId) {
86
+ return null;
87
+ }
88
+
89
+ const user = await this.db.query.users.findFirst({
90
+ where: eq(users.id, userId),
91
+ });
92
+
93
+ if (!user) {
94
+ return null;
95
+ }
96
+
97
+ return this.sanitizeUser(user);
98
+ }
99
+
100
+ async updateUserById(userId: string, updateUserDto: UpdateUserDto) {
101
+ // Check if user exists
102
+ const user = await this.db.query.users.findFirst({
103
+ where: eq(users.id, userId),
104
+ });
105
+
106
+ if (!user) {
107
+ throw new NotFoundException('User not found');
108
+ }
109
+
110
+ const updateData: Record<string, any> = { updatedAt: new Date() };
111
+ if (updateUserDto.fullName) updateData.fullName = updateUserDto.fullName;
112
+ if (updateUserDto.role) updateData.role = updateUserDto.role as UserRole;
113
+ if (updateUserDto.isActive !== undefined) updateData.isActive = updateUserDto.isActive;
114
+
115
+ const [updatedUser] = await this.db
116
+ .update(users)
117
+ .set(updateData)
118
+ .where(eq(users.id, userId))
119
+ .returning();
120
+
121
+ return this.sanitizeUser(updatedUser);
122
+ }
123
+
124
+ async deleteUserById(userId: string) {
125
+ const user = await this.db.query.users.findFirst({
126
+ where: eq(users.id, userId),
127
+ });
128
+
129
+ if (!user) {
130
+ throw new NotFoundException('User not found');
131
+ }
132
+
133
+ // Delete user's refresh tokens first
134
+ await this.db.delete(refreshTokens).where(eq(refreshTokens.userId, userId));
135
+
136
+ // Delete user
137
+ await this.db.delete(users).where(eq(users.id, userId));
138
+
139
+ return { message: 'User deleted successfully' };
140
+ }
141
+
142
+ // Utils
143
+
144
+ private sanitizeUser(user: any) {
145
+ if (!user) {
146
+ return null;
147
+ }
148
+
149
+ const { passwordHash, ...safeUser } = user;
150
+ return safeUser;
151
+ }
152
+ }
@@ -0,0 +1,22 @@
1
+ # Database Configuration (MongoDB)
2
+ DATABASE_URL=mongodb://user:password@localhost:27017/database_name
3
+
4
+ # JWT Configuration
5
+ JWT_ACCESS_SECRET=your_jwt_access_secret_key_here_minimum_32_characters
6
+ JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here_minimum_32_characters
7
+ # Token expiration (format: {number}{unit} where unit = s|m|h|d)
8
+ JWT_ACCESS_EXPIRY=60m
9
+ JWT_REFRESH_EXPIRY=30d
10
+
11
+ # Server Configuration
12
+ PORT=8080
13
+ NODE_ENV=development
14
+
15
+ # CORS Configuration
16
+ # Single origin: http://localhost:3000
17
+ # Multiple origins: http://localhost:3000,https://app.example.com
18
+ # Wildcard (use with caution): *
19
+ CORS_ORIGIN=http://localhost:3000
20
+
21
+ # Logging
22
+ LOG_LEVEL=info
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "nestjs-auth-mongoose",
3
+ "version": "0.0.1",
4
+ "description": "NestJS Authentication API with Mongoose & MongoDB",
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
+ "db:seed": "ts-node src/database/seed.ts"
22
+ },
23
+ "dependencies": {
24
+ "@nestjs/common": "^11.0.0",
25
+ "@nestjs/config": "^4.0.0",
26
+ "@nestjs/core": "^11.0.0",
27
+ "@nestjs/jwt": "^11.0.0",
28
+ "@nestjs/mongoose": "^11.0.0",
29
+ "@nestjs/platform-express": "^11.0.0",
30
+ "@nestjs/throttler": "^6.4.0",
31
+ "bcrypt": "^6.0.0",
32
+ "class-transformer": "^0.5.1",
33
+ "class-validator": "^0.14.2",
34
+ "cookie-parser": "^1.4.7",
35
+ "helmet": "^8.1.0",
36
+ "zod": "^4.1.12",
37
+ "mongoose": "^8.9.0",
38
+ "nestjs-pino": "^4.2.0",
39
+ "pino-http": "^10.4.0",
40
+ "pino-pretty": "^13.0.0",
41
+ "reflect-metadata": "^0.2.2",
42
+ "rxjs": "^7.8.1"
43
+ },
44
+ "devDependencies": {
45
+ "@nestjs/cli": "^11.0.0",
46
+ "@nestjs/schematics": "^11.0.0",
47
+ "@nestjs/testing": "^11.0.0",
48
+ "@types/bcrypt": "^6.0.0",
49
+ "@types/cookie-parser": "^1.4.10",
50
+ "@types/express": "^5.0.0",
51
+ "@types/jest": "^30.0.0",
52
+ "@types/node": "^22.10.0",
53
+ "@types/supertest": "^6.0.2",
54
+ "dotenv": "^16.4.7",
55
+ "eslint": "^9.16.0",
56
+ "eslint-config-prettier": "^10.0.0",
57
+ "eslint-plugin-prettier": "^5.2.2",
58
+ "jest": "^30.0.0",
59
+ "prettier": "^3.4.2",
60
+ "source-map-support": "^0.5.21",
61
+ "supertest": "^7.0.0",
62
+ "ts-jest": "^29.2.5",
63
+ "ts-loader": "^9.5.1",
64
+ "ts-node": "^10.9.2",
65
+ "tsconfig-paths": "^4.2.0",
66
+ "typescript": "^5.7.2",
67
+ "typescript-eslint": "^8.18.0"
68
+ },
69
+ "jest": {
70
+ "moduleFileExtensions": [
71
+ "js",
72
+ "json",
73
+ "ts"
74
+ ],
75
+ "rootDir": "src",
76
+ "testRegex": ".*\\.spec\\.ts$",
77
+ "transform": {
78
+ "^.+\\.(t|j)s$": "ts-jest"
79
+ },
80
+ "collectCoverageFrom": [
81
+ "**/*.(t|j)s"
82
+ ],
83
+ "coverageDirectory": "../coverage",
84
+ "testEnvironment": "node"
85
+ }
86
+ }
@@ -0,0 +1,73 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { ConfigModule } from '@nestjs/config';
3
+ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
4
+ import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
5
+ import { LoggerModule } from 'nestjs-pino';
6
+
7
+ import { loggerConfig } from './config/logger.config';
8
+ import { validate } from './config/env.validation';
9
+ import { DatabaseModule } from './database/database.module';
10
+ import { AuthModule } from './modules/auth/auth.module';
11
+ import { UsersModule } from './modules/users/users.module';
12
+ import { HealthModule } from './modules/health/health.module';
13
+ import { AuthGuard } from './common/guards/auth.guard';
14
+ import { RolesGuard } from './common/guards/roles.guard';
15
+ import { HttpExceptionFilter } from './common/filters/http-exception.filter';
16
+ import { ResponseInterceptor } from './common/interceptors/response.interceptor';
17
+
18
+ @Module({
19
+ imports: [
20
+ // Configuration
21
+ ConfigModule.forRoot({
22
+ isGlobal: true,
23
+ validate,
24
+ }),
25
+
26
+ // Logging
27
+ LoggerModule.forRoot(loggerConfig),
28
+
29
+ // Rate limiting
30
+ ThrottlerModule.forRoot([
31
+ {
32
+ ttl: 60000, // 1 minute
33
+ limit: 100, // 100 requests per minute
34
+ },
35
+ ]),
36
+
37
+ // Database (MongoDB via Mongoose)
38
+ DatabaseModule,
39
+
40
+ // Feature modules
41
+ AuthModule,
42
+ UsersModule,
43
+ HealthModule,
44
+ ],
45
+ providers: [
46
+ // Global exception filter
47
+ {
48
+ provide: APP_FILTER,
49
+ useClass: HttpExceptionFilter,
50
+ },
51
+ // Global response interceptor
52
+ {
53
+ provide: APP_INTERCEPTOR,
54
+ useClass: ResponseInterceptor,
55
+ },
56
+ // Global rate limiter
57
+ {
58
+ provide: APP_GUARD,
59
+ useClass: ThrottlerGuard,
60
+ },
61
+ // Global auth guard
62
+ {
63
+ provide: APP_GUARD,
64
+ useClass: AuthGuard,
65
+ },
66
+ // Global roles guard
67
+ {
68
+ provide: APP_GUARD,
69
+ useClass: RolesGuard,
70
+ },
71
+ ],
72
+ })
73
+ export class AppModule {}