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,55 @@
1
+ import { z } from 'zod';
2
+
3
+ export const envSchema = z.object({
4
+ NODE_ENV: z
5
+ .enum(['development', 'production', 'test'])
6
+ .default('development'),
7
+ PORT: z.coerce.number().default(8080),
8
+
9
+ // Database
10
+ DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'),
11
+
12
+ // JWT Secrets
13
+ JWT_ACCESS_SECRET: z
14
+ .string()
15
+ .min(32, 'JWT_ACCESS_SECRET must be at least 32 characters'),
16
+ JWT_REFRESH_SECRET: z
17
+ .string()
18
+ .min(32, 'JWT_REFRESH_SECRET must be at least 32 characters'),
19
+ JWT_ACCESS_EXPIRY: z
20
+ .string()
21
+ .regex(/^\d+[smhd]$/, 'JWT_ACCESS_EXPIRY must be in format: 60m, 1h, etc.')
22
+ .default('15m'),
23
+ JWT_REFRESH_EXPIRY: z
24
+ .string()
25
+ .regex(/^\d+[smhd]$/, 'JWT_REFRESH_EXPIRY must be in format: 7d, 30d, etc.')
26
+ .default('7d'),
27
+
28
+ // CORS
29
+ CORS_ORIGIN: z
30
+ .string()
31
+ .refine((val) => {
32
+ const origins = val.split(',').map((o) => o.trim());
33
+ const urlRegex = /^https?:\/\/.+/;
34
+ return origins.every((origin) => urlRegex.test(origin) || origin === '*');
35
+ }, 'CORS_ORIGIN must be valid URL(s) or "*"')
36
+ .default('http://localhost:3000'),
37
+
38
+ // Logging
39
+ LOG_LEVEL: z
40
+ .enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace'])
41
+ .default('info'),
42
+ });
43
+
44
+ export type Env = z.infer<typeof envSchema>;
45
+
46
+ export function validate(config: Record<string, unknown>) {
47
+ const result = envSchema.safeParse(config);
48
+ if (!result.success) {
49
+ const errors = result.error.issues
50
+ .map((e) => `${e.path.join('.')}: ${e.message}`)
51
+ .join('\n');
52
+ throw new Error(`Environment validation failed:\n${errors}`);
53
+ }
54
+ return result.data;
55
+ }
@@ -0,0 +1,19 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { MongooseModule } from '@nestjs/mongoose';
3
+ import { ConfigModule, ConfigService } from '@nestjs/config';
4
+
5
+ @Module({
6
+ imports: [
7
+ MongooseModule.forRootAsync({
8
+ imports: [ConfigModule],
9
+ useFactory: (configService: ConfigService) => ({
10
+ uri: configService.get<string>('DATABASE_URL'),
11
+ // Connection options
12
+ retryWrites: true,
13
+ w: 'majority',
14
+ }),
15
+ inject: [ConfigService],
16
+ }),
17
+ ],
18
+ })
19
+ export class DatabaseModule {}
@@ -0,0 +1,107 @@
1
+ import { connect, connection } from 'mongoose';
2
+ import * as bcrypt from 'bcrypt';
3
+ import * as dotenv from 'dotenv';
4
+
5
+ // Load environment variables
6
+ dotenv.config();
7
+
8
+ const SALT_ROUNDS = 12;
9
+
10
+ interface UserDoc {
11
+ fullName: string;
12
+ email: string;
13
+ passwordHash: string;
14
+ role: 'USER' | 'ADMIN';
15
+ isActive: boolean;
16
+ createdAt: Date;
17
+ updatedAt: Date;
18
+ }
19
+
20
+ async function seed() {
21
+ const databaseUrl = process.env.DATABASE_URL;
22
+
23
+ if (!databaseUrl) {
24
+ console.error('❌ DATABASE_URL is not defined in environment variables');
25
+ process.exit(1);
26
+ }
27
+
28
+ console.log('🌱 Starting database seed...\n');
29
+
30
+ try {
31
+ // Connect to MongoDB
32
+ await connect(databaseUrl);
33
+ console.log('✓ Connected to MongoDB');
34
+
35
+ const db = connection.db;
36
+ if (!db) {
37
+ throw new Error('Failed to get database instance from connection');
38
+ }
39
+ const usersCollection = db.collection('users');
40
+
41
+ // Check if admin already exists
42
+ const existingAdmin = await usersCollection.findOne({ email: 'admin@example.com' });
43
+
44
+ if (existingAdmin) {
45
+ console.log('⚠️ Admin user already exists, skipping...');
46
+ } else {
47
+ // Create admin user
48
+ const adminPasswordHash = await bcrypt.hash('Admin@123', SALT_ROUNDS);
49
+ const now = new Date();
50
+
51
+ const adminUser: UserDoc = {
52
+ fullName: 'Admin User',
53
+ email: 'admin@example.com',
54
+ passwordHash: adminPasswordHash,
55
+ role: 'ADMIN',
56
+ isActive: true,
57
+ createdAt: now,
58
+ updatedAt: now,
59
+ };
60
+
61
+ await usersCollection.insertOne(adminUser);
62
+ console.log('✓ Created admin user');
63
+ console.log(' Email: admin@example.com');
64
+ console.log(' Password: Admin@123');
65
+ }
66
+
67
+ // Check if regular user already exists
68
+ const existingUser = await usersCollection.findOne({ email: 'user@example.com' });
69
+
70
+ if (existingUser) {
71
+ console.log('⚠️ Regular user already exists, skipping...');
72
+ } else {
73
+ // Create regular user
74
+ const userPasswordHash = await bcrypt.hash('User@123', SALT_ROUNDS);
75
+ const now = new Date();
76
+
77
+ const regularUser: UserDoc = {
78
+ fullName: 'Regular User',
79
+ email: 'user@example.com',
80
+ passwordHash: userPasswordHash,
81
+ role: 'USER',
82
+ isActive: true,
83
+ createdAt: now,
84
+ updatedAt: now,
85
+ };
86
+
87
+ await usersCollection.insertOne(regularUser);
88
+ console.log('✓ Created regular user');
89
+ console.log(' Email: user@example.com');
90
+ console.log(' Password: User@123');
91
+ }
92
+
93
+ // Create indexes
94
+ console.log('\n📇 Creating indexes...');
95
+ await usersCollection.createIndex({ email: 1 }, { unique: true });
96
+ console.log('✓ Created unique index on users.email');
97
+
98
+ console.log('\n✅ Database seed completed successfully!\n');
99
+ } catch (error) {
100
+ console.error('❌ Seed failed:', error);
101
+ process.exit(1);
102
+ } finally {
103
+ await connection.close();
104
+ }
105
+ }
106
+
107
+ seed();
@@ -0,0 +1,21 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { JwtModule } from '@nestjs/jwt';
3
+ import { MongooseModule } from '@nestjs/mongoose';
4
+ import { AuthController } from './auth.controller';
5
+ import { AuthService } from './auth.service';
6
+ import { User, UserSchema } from '../../schemas/user.schema';
7
+ import { RefreshToken, RefreshTokenSchema } from '../../schemas/refresh-token.schema';
8
+
9
+ @Module({
10
+ imports: [
11
+ MongooseModule.forFeature([
12
+ { name: User.name, schema: UserSchema },
13
+ { name: RefreshToken.name, schema: RefreshTokenSchema },
14
+ ]),
15
+ JwtModule.register({}),
16
+ ],
17
+ controllers: [AuthController],
18
+ providers: [AuthService],
19
+ exports: [AuthService, JwtModule],
20
+ })
21
+ export class AuthModule {}
@@ -0,0 +1,272 @@
1
+ import {
2
+ Injectable,
3
+ UnauthorizedException,
4
+ ConflictException,
5
+ ForbiddenException,
6
+ NotFoundException,
7
+ } from '@nestjs/common';
8
+ import { ConfigService } from '@nestjs/config';
9
+ import { JwtService } from '@nestjs/jwt';
10
+ import { InjectModel } from '@nestjs/mongoose';
11
+ import { Model } from 'mongoose';
12
+ import * as bcrypt from 'bcrypt';
13
+ import { User, UserDocument, UserRole } from '../../schemas/user.schema';
14
+ import { RefreshToken, RefreshTokenDocument } from '../../schemas/refresh-token.schema';
15
+ import { SignupDto } from './dtos/signup.dto';
16
+ import { LoginDto } from './dtos/login.dto';
17
+
18
+ const SALT_ROUNDS = 12;
19
+
20
+ @Injectable()
21
+ export class AuthService {
22
+ constructor(
23
+ @InjectModel(User.name) private userModel: Model<UserDocument>,
24
+ @InjectModel(RefreshToken.name) private refreshTokenModel: Model<RefreshTokenDocument>,
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.userModel.findOne({ email: email.toLowerCase() });
34
+ if (existingUser) {
35
+ throw new ConflictException('User with this email already exists');
36
+ }
37
+
38
+ // Hash password and create user
39
+ const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
40
+ const user = await this.userModel.create({
41
+ fullName,
42
+ email: email.toLowerCase(),
43
+ passwordHash,
44
+ role: UserRole.USER,
45
+ });
46
+
47
+ return {
48
+ id: user._id.toString(),
49
+ fullName: user.fullName,
50
+ email: user.email,
51
+ role: user.role,
52
+ };
53
+ }
54
+
55
+ async login(loginDto: LoginDto, deviceInfo?: string, ipAddress?: string) {
56
+ const { email, password } = loginDto;
57
+
58
+ // Find user
59
+ const user = await this.userModel.findOne({ email: email.toLowerCase() });
60
+ if (!user) {
61
+ throw new UnauthorizedException('Invalid email or password');
62
+ }
63
+
64
+ // Check if user is active
65
+ if (!user.isActive) {
66
+ throw new UnauthorizedException('Account is deactivated');
67
+ }
68
+
69
+ // Verify password
70
+ const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
71
+ if (!isPasswordValid) {
72
+ throw new UnauthorizedException('Invalid email or password');
73
+ }
74
+
75
+ // Generate tokens
76
+ const tokens = await this.generateTokens(user);
77
+
78
+ // Store refresh token in database
79
+ await this.storeRefreshToken(
80
+ tokens.refreshToken,
81
+ user._id.toString(),
82
+ deviceInfo || 'Unknown Device',
83
+ ipAddress || 'Unknown IP',
84
+ );
85
+
86
+ // Clean up expired tokens for this user
87
+ await this.cleanupExpiredTokens(user._id.toString());
88
+
89
+ return {
90
+ user: {
91
+ id: user._id.toString(),
92
+ fullName: user.fullName,
93
+ email: user.email,
94
+ role: user.role,
95
+ },
96
+ ...tokens,
97
+ };
98
+ }
99
+
100
+ async logout(userId: string, refreshToken?: string) {
101
+ if (refreshToken) {
102
+ // Find and revoke the specific refresh token by comparing hashes
103
+ const storedTokens = await this.refreshTokenModel.find({ userId });
104
+
105
+ for (const storedToken of storedTokens) {
106
+ const matches = await bcrypt.compare(refreshToken, storedToken.token);
107
+ if (matches) {
108
+ await this.refreshTokenModel.updateOne(
109
+ { _id: storedToken._id },
110
+ { isRevoked: true },
111
+ );
112
+ break;
113
+ }
114
+ }
115
+ } else {
116
+ // Logout from all devices - revoke all refresh tokens for this user
117
+ await this.refreshTokenModel.updateMany(
118
+ { userId, isRevoked: false },
119
+ { isRevoked: true },
120
+ );
121
+ }
122
+
123
+ return { message: 'Logged out successfully' };
124
+ }
125
+
126
+ async refreshToken(
127
+ userId: string,
128
+ rt: string,
129
+ deviceInfo?: string,
130
+ ipAddress?: string,
131
+ ) {
132
+ // Find the user
133
+ const user = await this.userModel.findById(userId);
134
+ if (!user) {
135
+ throw new ForbiddenException('Invalid refresh token');
136
+ }
137
+
138
+ // Find matching refresh token in database by comparing hashes
139
+ const storedTokens = await this.refreshTokenModel.find({
140
+ userId,
141
+ isRevoked: false,
142
+ expiresAt: { $gt: new Date() },
143
+ });
144
+
145
+ let validToken: RefreshTokenDocument | null = null;
146
+ for (const storedToken of storedTokens) {
147
+ const matches = await bcrypt.compare(rt, storedToken.token);
148
+ if (matches) {
149
+ validToken = storedToken;
150
+ break;
151
+ }
152
+ }
153
+
154
+ if (!validToken) {
155
+ throw new UnauthorizedException('Invalid refresh token');
156
+ }
157
+
158
+ // Revoke old refresh token (token rotation)
159
+ await this.refreshTokenModel.updateOne(
160
+ { _id: validToken._id },
161
+ { isRevoked: true },
162
+ );
163
+
164
+ // Generate new tokens
165
+ const tokens = await this.generateTokens(user);
166
+
167
+ // Store new refresh token
168
+ await this.storeRefreshToken(
169
+ tokens.refreshToken,
170
+ user._id.toString(),
171
+ deviceInfo || 'Unknown Device',
172
+ ipAddress || 'Unknown IP',
173
+ );
174
+
175
+ return tokens;
176
+ }
177
+
178
+ async getMe(userId: string) {
179
+ const user = await this.userModel.findById(userId);
180
+ if (!user) {
181
+ throw new NotFoundException('User not found');
182
+ }
183
+
184
+ return {
185
+ id: user._id.toString(),
186
+ fullName: user.fullName,
187
+ email: user.email,
188
+ role: user.role,
189
+ };
190
+ }
191
+
192
+ // Helper Methods
193
+
194
+ async hashData(data: string): Promise<string> {
195
+ return bcrypt.hash(data, SALT_ROUNDS);
196
+ }
197
+
198
+ async generateTokens(user: UserDocument): Promise<{ accessToken: string; refreshToken: string }> {
199
+ const payload = {
200
+ sub: user._id.toString(),
201
+ email: user.email,
202
+ role: user.role,
203
+ };
204
+
205
+ const accessExpiry = (this.configService.get<string>('JWT_ACCESS_EXPIRY') || '15m') as any;
206
+ const refreshExpiry = (this.configService.get<string>('JWT_REFRESH_EXPIRY') || '7d') as any;
207
+
208
+ const [accessToken, refreshToken] = await Promise.all([
209
+ this.jwtService.signAsync(payload, {
210
+ secret: this.configService.get<string>('JWT_ACCESS_SECRET'),
211
+ expiresIn: accessExpiry,
212
+ }),
213
+ this.jwtService.signAsync(payload, {
214
+ secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
215
+ expiresIn: refreshExpiry,
216
+ }),
217
+ ]);
218
+
219
+ return { accessToken, refreshToken };
220
+ }
221
+
222
+ private async storeRefreshToken(
223
+ token: string,
224
+ userId: string,
225
+ userAgent: string,
226
+ ipAddress: string,
227
+ ) {
228
+ const expiresIn = this.configService.get<string>('JWT_REFRESH_EXPIRY') || '7d';
229
+ const expiresAt = this.calculateExpiry(expiresIn);
230
+
231
+ // Hash the refresh token before storing
232
+ const hashedToken = await this.hashData(token);
233
+
234
+ await this.refreshTokenModel.create({
235
+ token: hashedToken,
236
+ userId,
237
+ userAgent,
238
+ ipAddress,
239
+ expiresAt,
240
+ });
241
+ }
242
+
243
+ private calculateExpiry(expiresIn: string): Date {
244
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
245
+ if (!match) {
246
+ return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // Default 7 days
247
+ }
248
+
249
+ const value = parseInt(match[1], 10);
250
+ const unit = match[2];
251
+
252
+ const multipliers: Record<string, number> = {
253
+ s: 1000,
254
+ m: 60 * 1000,
255
+ h: 60 * 60 * 1000,
256
+ d: 24 * 60 * 60 * 1000,
257
+ };
258
+
259
+ return new Date(Date.now() + value * multipliers[unit]);
260
+ }
261
+
262
+ private async cleanupExpiredTokens(userId: string) {
263
+ // Remove expired/revoked refresh tokens for this user
264
+ await this.refreshTokenModel.deleteMany({
265
+ userId,
266
+ $or: [
267
+ { isRevoked: true },
268
+ { expiresAt: { $lt: new Date() } },
269
+ ],
270
+ });
271
+ }
272
+ }
@@ -0,0 +1,10 @@
1
+ import { IsEmail, IsString, MinLength } from 'class-validator';
2
+
3
+ export class LoginDto {
4
+ @IsEmail()
5
+ email: string;
6
+
7
+ @IsString()
8
+ @MinLength(6)
9
+ password: string;
10
+ }
@@ -0,0 +1,8 @@
1
+ import { IsString, MinLength } from 'class-validator';
2
+ import { LoginDto } from './login.dto';
3
+
4
+ export class SignupDto extends LoginDto {
5
+ @IsString()
6
+ @MinLength(2)
7
+ fullName: string;
8
+ }
@@ -0,0 +1,26 @@
1
+ import { Controller, Get } from '@nestjs/common';
2
+ import { Connection } from 'mongoose';
3
+ import { InjectConnection } from '@nestjs/mongoose';
4
+ import { Public } from '../../common/decorators/public.decorator';
5
+
6
+ @Controller('health')
7
+ export class HealthController {
8
+ constructor(@InjectConnection() private connection: Connection) {}
9
+
10
+ @Public()
11
+ @Get()
12
+ async check() {
13
+ const dbStatus = this.connection.readyState === 1 ? 'healthy' : 'unhealthy';
14
+
15
+ return {
16
+ status: dbStatus === 'healthy' ? 'ok' : 'error',
17
+ timestamp: new Date().toISOString(),
18
+ services: {
19
+ database: {
20
+ status: dbStatus,
21
+ type: 'mongodb',
22
+ },
23
+ },
24
+ };
25
+ }
26
+ }
@@ -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,24 @@
1
+ import {
2
+ IsEmail,
3
+ IsOptional,
4
+ IsString,
5
+ MaxLength,
6
+ MinLength,
7
+ } from 'class-validator';
8
+
9
+ export class UpdateProfileDto {
10
+ @IsOptional()
11
+ @IsString()
12
+ @MinLength(2)
13
+ @MaxLength(100)
14
+ fullName?: string;
15
+
16
+ @IsOptional()
17
+ @IsEmail()
18
+ email?: string;
19
+
20
+ @IsOptional()
21
+ @IsString()
22
+ @MinLength(6)
23
+ password?: string;
24
+ }
@@ -0,0 +1,13 @@
1
+ import { IsBoolean, IsEnum, IsOptional } from 'class-validator';
2
+ import { UpdateProfileDto } from './update-profile.dto';
3
+ import { UserRole } from '../../../schemas/user.schema';
4
+
5
+ export class UpdateUserDto extends UpdateProfileDto {
6
+ @IsOptional()
7
+ @IsEnum(UserRole, { message: 'Role must be a valid UserRole' })
8
+ role?: UserRole;
9
+
10
+ @IsOptional()
11
+ @IsBoolean()
12
+ isActive?: boolean;
13
+ }
@@ -0,0 +1,19 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { MongooseModule } from '@nestjs/mongoose';
3
+ import { UsersController } from './users.controller';
4
+ import { UsersService } from './users.service';
5
+ import { User, UserSchema } from '../../schemas/user.schema';
6
+ import { RefreshToken, RefreshTokenSchema } from '../../schemas/refresh-token.schema';
7
+
8
+ @Module({
9
+ imports: [
10
+ MongooseModule.forFeature([
11
+ { name: User.name, schema: UserSchema },
12
+ { name: RefreshToken.name, schema: RefreshTokenSchema },
13
+ ]),
14
+ ],
15
+ controllers: [UsersController],
16
+ providers: [UsersService],
17
+ exports: [UsersService],
18
+ })
19
+ export class UsersModule {}