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,179 @@
1
+ import {
2
+ Injectable,
3
+ NotFoundException,
4
+ ConflictException,
5
+ } from '@nestjs/common';
6
+ import { InjectModel } from '@nestjs/mongoose';
7
+ import { Model } from 'mongoose';
8
+ import * as bcrypt from 'bcrypt';
9
+ import { User, UserDocument, UserRole } from '../../schemas/user.schema';
10
+ import { RefreshToken, RefreshTokenDocument } from '../../schemas/refresh-token.schema';
11
+ import { UpdateProfileDto } from './dtos/update-profile.dto';
12
+ import { UpdateUserDto } from './dtos/update-user.dto';
13
+
14
+ const SALT_ROUNDS = 12;
15
+
16
+ @Injectable()
17
+ export class UsersService {
18
+ constructor(
19
+ @InjectModel(User.name) private userModel: Model<UserDocument>,
20
+ @InjectModel(RefreshToken.name) private refreshTokenModel: Model<RefreshTokenDocument>,
21
+ ) {}
22
+
23
+ async getProfile(userId: string) {
24
+ const user = await this.userModel.findById(userId).select('-passwordHash');
25
+ if (!user) {
26
+ throw new NotFoundException('User not found');
27
+ }
28
+ return this.sanitizeUser(user);
29
+ }
30
+
31
+ async updateProfile(userId: string, updateProfileDto: UpdateProfileDto) {
32
+ const user = await this.userModel.findById(userId);
33
+ if (!user) {
34
+ throw new NotFoundException('User not found');
35
+ }
36
+
37
+ // Check if email is being changed and if it's already taken
38
+ if (updateProfileDto.email && updateProfileDto.email !== user.email) {
39
+ const existingUser = await this.userModel.findOne({
40
+ email: updateProfileDto.email.toLowerCase(),
41
+ _id: { $ne: userId },
42
+ });
43
+ if (existingUser) {
44
+ throw new ConflictException('Email already in use');
45
+ }
46
+ }
47
+
48
+ // Hash new password if provided
49
+ let passwordHash: string | undefined;
50
+ if (updateProfileDto.password) {
51
+ passwordHash = await bcrypt.hash(updateProfileDto.password, SALT_ROUNDS);
52
+ }
53
+
54
+ const updateData: any = {};
55
+ if (updateProfileDto.fullName) updateData.fullName = updateProfileDto.fullName;
56
+ if (updateProfileDto.email) updateData.email = updateProfileDto.email.toLowerCase();
57
+ if (passwordHash) updateData.passwordHash = passwordHash;
58
+
59
+ const updatedUser = await this.userModel
60
+ .findByIdAndUpdate(userId, updateData, { new: true })
61
+ .select('-passwordHash');
62
+
63
+ return this.sanitizeUser(updatedUser);
64
+ }
65
+
66
+ // Admin methods
67
+
68
+ async getAllUsers(page: number = 1, limit: number = 10) {
69
+ const skip = (page - 1) * limit;
70
+
71
+ const [usersList, total] = await Promise.all([
72
+ this.userModel
73
+ .find()
74
+ .select('-passwordHash')
75
+ .skip(skip)
76
+ .limit(limit)
77
+ .sort({ createdAt: -1 }),
78
+ this.userModel.countDocuments(),
79
+ ]);
80
+
81
+ const totalPages = Math.ceil(total / limit);
82
+
83
+ return {
84
+ data: usersList.map((user) => this.sanitizeUser(user)),
85
+ meta: {
86
+ total,
87
+ page,
88
+ limit,
89
+ totalPages,
90
+ hasNext: page < totalPages,
91
+ hasPrevious: page > 1,
92
+ },
93
+ };
94
+ }
95
+
96
+ async getUserById(userId: string) {
97
+ if (!userId) {
98
+ return null;
99
+ }
100
+
101
+ const user = await this.userModel.findById(userId).select('-passwordHash');
102
+ if (!user) {
103
+ return null;
104
+ }
105
+
106
+ return this.sanitizeUser(user);
107
+ }
108
+
109
+ async updateUserById(userId: string, updateUserDto: UpdateUserDto) {
110
+ // Check if user exists
111
+ const user = await this.userModel.findById(userId);
112
+ if (!user) {
113
+ throw new NotFoundException('User not found');
114
+ }
115
+
116
+ // Check if email is being changed and if it's already taken
117
+ if (updateUserDto.email && updateUserDto.email !== user.email) {
118
+ const existingUser = await this.userModel.findOne({
119
+ email: updateUserDto.email.toLowerCase(),
120
+ _id: { $ne: userId },
121
+ });
122
+ if (existingUser) {
123
+ throw new ConflictException('Email already in use');
124
+ }
125
+ }
126
+
127
+ // Hash new password if provided
128
+ let passwordHash: string | undefined;
129
+ if (updateUserDto.password) {
130
+ passwordHash = await bcrypt.hash(updateUserDto.password, SALT_ROUNDS);
131
+ }
132
+
133
+ const updateData: any = {};
134
+ if (updateUserDto.fullName) updateData.fullName = updateUserDto.fullName;
135
+ if (updateUserDto.email) updateData.email = updateUserDto.email.toLowerCase();
136
+ if (updateUserDto.role) updateData.role = updateUserDto.role;
137
+ if (updateUserDto.isActive !== undefined) updateData.isActive = updateUserDto.isActive;
138
+ if (passwordHash) updateData.passwordHash = passwordHash;
139
+
140
+ const updatedUser = await this.userModel
141
+ .findByIdAndUpdate(userId, updateData, { new: true })
142
+ .select('-passwordHash');
143
+
144
+ return this.sanitizeUser(updatedUser);
145
+ }
146
+
147
+ async deleteUserById(userId: string) {
148
+ const user = await this.userModel.findById(userId);
149
+ if (!user) {
150
+ throw new NotFoundException('User not found');
151
+ }
152
+
153
+ // Delete user's refresh tokens
154
+ await this.refreshTokenModel.deleteMany({ userId });
155
+
156
+ // Delete user
157
+ await this.userModel.findByIdAndDelete(userId);
158
+
159
+ return { message: 'User deleted successfully' };
160
+ }
161
+
162
+ // Utils
163
+
164
+ private sanitizeUser(user: UserDocument | null) {
165
+ if (!user) {
166
+ return null;
167
+ }
168
+
169
+ return {
170
+ id: user._id.toString(),
171
+ fullName: user.fullName,
172
+ email: user.email,
173
+ role: user.role,
174
+ isActive: user.isActive,
175
+ createdAt: user.createdAt,
176
+ updatedAt: user.updatedAt,
177
+ };
178
+ }
179
+ }
@@ -0,0 +1,2 @@
1
+ export * from './user.schema';
2
+ export * from './refresh-token.schema';
@@ -0,0 +1,55 @@
1
+ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2
+ import { Document, Types } from 'mongoose';
3
+
4
+ export type RefreshTokenDocument = RefreshToken & Document;
5
+
6
+ @Schema({
7
+ timestamps: true,
8
+ toJSON: {
9
+ transform: (_, ret: Record<string, any>) => {
10
+ ret.id = ret._id.toString();
11
+ delete ret._id;
12
+ delete ret.__v;
13
+ return ret;
14
+ },
15
+ },
16
+ })
17
+ export class RefreshToken {
18
+ _id: Types.ObjectId;
19
+
20
+ @Prop({ required: true })
21
+ token: string;
22
+
23
+ @Prop({ type: Types.ObjectId, ref: 'User', required: true })
24
+ userId: Types.ObjectId;
25
+
26
+ @Prop({ required: true })
27
+ userAgent: string;
28
+
29
+ @Prop({ required: true })
30
+ ipAddress: string;
31
+
32
+ @Prop({ required: true })
33
+ expiresAt: Date;
34
+
35
+ @Prop({ default: false })
36
+ isRevoked: boolean;
37
+
38
+ @Prop({ default: Date.now })
39
+ createdAt: Date;
40
+
41
+ @Prop({ default: Date.now })
42
+ updatedAt: Date;
43
+
44
+ // Virtual for id
45
+ get id(): string {
46
+ return this._id.toString();
47
+ }
48
+ }
49
+
50
+ export const RefreshTokenSchema = SchemaFactory.createForClass(RefreshToken);
51
+
52
+ // Add indexes for common queries
53
+ RefreshTokenSchema.index({ token: 1 });
54
+ RefreshTokenSchema.index({ userId: 1 });
55
+ RefreshTokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); // TTL index for auto-cleanup
@@ -0,0 +1,53 @@
1
+ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2
+ import { Document, Types } from 'mongoose';
3
+
4
+ export type UserDocument = User & Document;
5
+
6
+ export enum UserRole {
7
+ USER = 'USER',
8
+ ADMIN = 'ADMIN',
9
+ }
10
+
11
+ @Schema({
12
+ timestamps: true,
13
+ toJSON: {
14
+ transform: (_, ret: Record<string, any>) => {
15
+ ret.id = ret._id.toString();
16
+ delete ret._id;
17
+ delete ret.__v;
18
+ delete ret.passwordHash;
19
+ return ret;
20
+ },
21
+ },
22
+ })
23
+ export class User {
24
+ _id: Types.ObjectId;
25
+
26
+ @Prop({ required: true })
27
+ fullName: string;
28
+
29
+ @Prop({ required: true, unique: true, lowercase: true, trim: true })
30
+ email: string;
31
+
32
+ @Prop({ required: true })
33
+ passwordHash: string;
34
+
35
+ @Prop({ type: String, enum: UserRole, default: UserRole.USER })
36
+ role: UserRole;
37
+
38
+ @Prop({ default: true })
39
+ isActive: boolean;
40
+
41
+ @Prop({ default: Date.now })
42
+ createdAt: Date;
43
+
44
+ @Prop({ default: Date.now })
45
+ updatedAt: Date;
46
+
47
+ // Virtual for id
48
+ get id(): string {
49
+ return this._id.toString();
50
+ }
51
+ }
52
+
53
+ export const UserSchema = SchemaFactory.createForClass(User);
@@ -0,0 +1,102 @@
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
+ "prisma:generate": "prisma generate",
22
+ "prisma:migrate": "prisma migrate dev",
23
+ "prisma:migrate:create": "prisma migrate dev --create-only",
24
+ "prisma:migrate:deploy": "prisma migrate deploy",
25
+ "prisma:migrate:status": "prisma migrate status",
26
+ "prisma:seed": "ts-node prisma/seed.ts",
27
+ "prisma:studio": "prisma studio",
28
+ "prisma:reset": "prisma migrate reset --force"
29
+ },
30
+ "prisma": {
31
+ "seed": "ts-node prisma/seed.ts"
32
+ },
33
+ "dependencies": {
34
+ "@nestjs/common": "^11.0.1",
35
+ "@nestjs/config": "^4.0.2",
36
+ "@nestjs/core": "^11.0.1",
37
+ "@nestjs/jwt": "^11.0.1",
38
+ "@nestjs/mapped-types": "*",
39
+ "@nestjs/platform-express": "^11.0.1",
40
+ "@nestjs/schedule": "^6.0.1",
41
+ "@nestjs/terminus": "^11.0.0",
42
+ "@nestjs/throttler": "^6.4.0",
43
+ "@prisma/client": "^6.19.0",
44
+ "bcrypt": "^6.0.0",
45
+ "class-transformer": "^0.5.1",
46
+ "class-validator": "^0.14.2",
47
+ "cookie-parser": "^1.4.7",
48
+ "dotenv": "^17.2.3",
49
+ "helmet": "^8.1.0",
50
+ "nestjs-pino": "^4.4.1",
51
+ "pino-http": "^10.5.0",
52
+ "pino-pretty": "^13.1.2",
53
+ "reflect-metadata": "^0.2.2",
54
+ "rxjs": "^7.8.1",
55
+ "zod": "^4.1.12"
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/eslintrc": "^3.2.0",
59
+ "@eslint/js": "^9.18.0",
60
+ "@nestjs/cli": "^11.0.0",
61
+ "@nestjs/schematics": "^11.0.0",
62
+ "@nestjs/testing": "^11.0.1",
63
+ "@types/bcrypt": "^6.0.0",
64
+ "@types/cookie-parser": "^1.4.10",
65
+ "@types/express": "^5.0.0",
66
+ "@types/jest": "^30.0.0",
67
+ "@types/node": "^22.10.7",
68
+ "@types/supertest": "^6.0.2",
69
+ "eslint": "^9.18.0",
70
+ "eslint-config-prettier": "^10.0.1",
71
+ "eslint-plugin-prettier": "^5.2.2",
72
+ "globals": "^16.0.0",
73
+ "jest": "^30.0.0",
74
+ "prettier": "^3.4.2",
75
+ "prisma": "^6.19.0",
76
+ "source-map-support": "^0.5.21",
77
+ "supertest": "^7.0.0",
78
+ "ts-jest": "^29.2.5",
79
+ "ts-loader": "^9.5.2",
80
+ "ts-node": "^10.9.2",
81
+ "tsconfig-paths": "^4.2.0",
82
+ "typescript": "^5.7.3",
83
+ "typescript-eslint": "^8.20.0"
84
+ },
85
+ "jest": {
86
+ "moduleFileExtensions": [
87
+ "js",
88
+ "json",
89
+ "ts"
90
+ ],
91
+ "rootDir": "src",
92
+ "testRegex": ".*\\.spec\\.ts$",
93
+ "transform": {
94
+ "^.+\\.(t|j)s$": "ts-jest"
95
+ },
96
+ "collectCoverageFrom": [
97
+ "**/*.(t|j)s"
98
+ ],
99
+ "coverageDirectory": "../coverage",
100
+ "testEnvironment": "node"
101
+ }
102
+ }
@@ -0,0 +1,60 @@
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5
+ // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6
+
7
+ generator client {
8
+ provider = "prisma-client-js"
9
+ }
10
+
11
+ datasource db {
12
+ provider = "postgresql"
13
+ url = env("DATABASE_URL")
14
+ }
15
+
16
+ // Enums
17
+ enum UserRole {
18
+ USER
19
+ ADMIN
20
+ }
21
+
22
+ // Tables
23
+ model User {
24
+ id String @id @default(uuid())
25
+ fullName String @db.VarChar(100)
26
+ email String @unique @db.VarChar(100)
27
+ passwordHash String @db.Text
28
+ role UserRole @default(USER)
29
+
30
+ refreshToken String? @db.Text // Legacy field - kept for backward compatibility
31
+ isActive Boolean @default(true) // for soft delete
32
+
33
+ // Relations
34
+ refreshTokens RefreshToken[]
35
+
36
+ // add fields as required
37
+
38
+ createdAt DateTime @default(now())
39
+ updatedAt DateTime @updatedAt
40
+
41
+ @@map("users")
42
+ }
43
+
44
+ model RefreshToken {
45
+ id String @id @default(uuid())
46
+ token String @db.Text
47
+ userId String
48
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
49
+
50
+ deviceInfo String? @db.VarChar(255) // Browser/device identifier
51
+ ipAddress String? @db.VarChar(45) // IPv4 or IPv6
52
+
53
+ expiresAt DateTime
54
+ createdAt DateTime @default(now())
55
+ updatedAt DateTime @updatedAt
56
+
57
+ @@index([userId])
58
+ @@index([token])
59
+ @@map("refresh_tokens")
60
+ }
@@ -0,0 +1,69 @@
1
+ import { PrismaClient, UserRole } from '@prisma/client';
2
+ import * as bcrypt from 'bcrypt';
3
+ import * as dotenv from 'dotenv';
4
+
5
+ // Load environment variables
6
+ dotenv.config();
7
+
8
+ const prisma = new PrismaClient();
9
+
10
+ async function main() {
11
+ console.log('🌱 Seeding database...');
12
+
13
+ // Use 12 rounds for bcrypt as per security standards
14
+ const adminPassword = await bcrypt.hash('Admin@123', 12);
15
+ const userPassword = await bcrypt.hash('User@123', 12);
16
+
17
+ // Create admin user
18
+ const admin = await prisma.user.upsert({
19
+ where: { email: 'admin@example.com' },
20
+ update: {},
21
+ create: {
22
+ email: 'admin@example.com',
23
+ passwordHash: adminPassword,
24
+ fullName: 'Admin User',
25
+ role: UserRole.ADMIN,
26
+ isActive: true,
27
+ },
28
+ });
29
+
30
+ // Create regular user
31
+ const user = await prisma.user.upsert({
32
+ where: { email: 'user@example.com' },
33
+ update: {},
34
+ create: {
35
+ email: 'user@example.com',
36
+ passwordHash: userPassword,
37
+ fullName: 'Regular User',
38
+ role: UserRole.USER,
39
+ isActive: true,
40
+ },
41
+ });
42
+
43
+ console.log('✅ Seeding completed');
44
+ console.log('\n📋 Test Credentials:');
45
+ console.log('===================');
46
+ console.log('Admin User:');
47
+ console.log(' Email:', admin.email);
48
+ console.log(' Password: Admin@123');
49
+ console.log(' Role:', admin.role);
50
+ console.log('\nRegular User:');
51
+ console.log(' Email:', user.email);
52
+ console.log(' Password: User@123');
53
+ console.log(' Role:', user.role);
54
+ console.log('\n⚠️ Note: Passwords meet strong validation requirements:');
55
+ console.log(' - Minimum 8 characters');
56
+ console.log(' - At least one uppercase letter');
57
+ console.log(' - At least one lowercase letter');
58
+ console.log(' - At least one number');
59
+ console.log(' - At least one special character');
60
+ }
61
+
62
+ main()
63
+ .catch((e) => {
64
+ console.error('❌ Seeding failed:', e);
65
+ process.exit(1);
66
+ })
67
+ .finally(async () => {
68
+ await prisma.$disconnect();
69
+ });
@@ -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 { PrismaModule } from './prisma/prisma.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, // 1 minute
21
+ limit: 10, // 10 requests per minute
22
+ },
23
+ {
24
+ name: 'strict',
25
+ ttl: 60000, // 1 minute
26
+ limit: 5, // 5 requests per minute for auth endpoints
27
+ },
28
+ ]),
29
+ AppConfigModule,
30
+ AuthModule,
31
+ UsersModule,
32
+ HealthModule,
33
+ PrismaModule,
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
+ }
@@ -0,0 +1,56 @@
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, 'Invalid DATABASE_URL'),
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('60m'),
23
+ JWT_REFRESH_EXPIRY: z
24
+ .string()
25
+ .regex(/^\d+[smhd]$/, 'JWT_REFRESH_EXPIRY must be in format: 7d, 30d, etc.')
26
+ .default('30d'),
27
+
28
+ // CORS
29
+ CORS_ORIGIN: z
30
+ .string()
31
+ .refine((val) => {
32
+ // Allow comma-separated origins
33
+ const origins = val.split(',').map((o) => o.trim());
34
+ const urlRegex = /^https?:\/\/.+/;
35
+ return origins.every((origin) => urlRegex.test(origin) || origin === '*');
36
+ }, 'CORS_ORIGIN must be valid URL(s) or "*". Multiple origins: "http://localhost:3000,https://example.com"')
37
+ .default('http://localhost:3000'),
38
+
39
+ // Logging
40
+ LOG_LEVEL: z
41
+ .enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace'])
42
+ .default('info'),
43
+ });
44
+
45
+ export type Env = z.infer<typeof envSchema>;
46
+
47
+ export function validate(config: Record<string, unknown>) {
48
+ const result = envSchema.safeParse(config);
49
+ if (!result.success) {
50
+ const errors = result.error.issues
51
+ .map((e) => `${e.path.join('.')}: ${e.message}`)
52
+ .join('\n');
53
+ throw new Error(`Environment validation failed:\n${errors}`);
54
+ }
55
+ return result.data;
56
+ }
@@ -0,0 +1,17 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { AuthService } from './auth.service';
3
+ import { AuthController } from './auth.controller';
4
+ import { PrismaModule } from 'src/prisma/prisma.module';
5
+ import { JwtModule } from '@nestjs/jwt';
6
+
7
+ @Module({
8
+ imports: [
9
+ PrismaModule,
10
+ JwtModule.register({
11
+ global: true,
12
+ }),
13
+ ],
14
+ controllers: [AuthController],
15
+ providers: [AuthService],
16
+ })
17
+ export class AuthModule {}