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.
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/cli.js +224 -0
- package/index.js +12 -0
- package/package.json +74 -0
- package/src/constants.js +65 -0
- package/src/generator.js +271 -0
- package/src/index.js +13 -0
- package/src/moduleGenerator.js +586 -0
- package/src/postSetup.js +365 -0
- package/src/prompts.js +189 -0
- package/src/utils.js +112 -0
- package/templates/README.md +81 -0
- package/templates/base/eslint.config.mjs +34 -0
- package/templates/base/gitignore +78 -0
- package/templates/base/nest-cli.json +8 -0
- package/templates/base/src/common/constants/cookie.config.ts +20 -0
- package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
- package/templates/base/src/common/decorators/public.decorator.ts +4 -0
- package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
- package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
- package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
- package/templates/base/src/common/guards/auth.guard.ts +51 -0
- package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
- package/templates/base/src/common/guards/roles.guard.ts +45 -0
- package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
- package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
- package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
- package/templates/base/src/common/validators/password.validator.ts +37 -0
- package/templates/base/src/config/config.module.ts +14 -0
- package/templates/base/src/config/logger.config.ts +109 -0
- package/templates/base/src/main.ts +84 -0
- package/templates/base/src/modules/auth/auth.controller.ts +133 -0
- package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
- package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/base/src/modules/health/health.controller.ts +20 -0
- package/templates/base/src/modules/health/health.module.ts +7 -0
- package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
- package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/base/src/modules/users/users.controller.ts +65 -0
- package/templates/base/test/app.e2e-spec.ts +24 -0
- package/templates/base/test/jest-e2e.json +9 -0
- package/templates/base/tsconfig.build.json +4 -0
- package/templates/base/tsconfig.json +24 -0
- package/templates/base-crud/CRUD_README.md +385 -0
- package/templates/base-crud/src/common/base/base.controller.ts +321 -0
- package/templates/base-crud/src/common/base/base.service.ts +192 -0
- package/templates/base-crud/src/common/base/index.ts +20 -0
- package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
- package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
- package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
- package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
- package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
- package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
- package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
- package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
- package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
- package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
- package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
- package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
- package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
- package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
- package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
- package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
- package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
- package/templates/database/mongodb/.env.example +22 -0
- package/templates/database/mysql/.env.example +24 -0
- package/templates/database/mysql/drizzle.config.ts +13 -0
- package/templates/database/mysql/package.json +5 -0
- package/templates/database/mysql/prisma/schema.prisma +55 -0
- package/templates/database/mysql/src/database/drizzle.ts +13 -0
- package/templates/database/mysql/src/database/schema.ts +58 -0
- package/templates/database/postgres/.env.example +24 -0
- package/templates/database/postgres/drizzle.config.ts +13 -0
- package/templates/database/postgres/package.json +8 -0
- package/templates/database/postgres/prisma/schema.prisma +55 -0
- package/templates/database/sqlite/.env.example +24 -0
- package/templates/database/sqlite/drizzle.config.ts +13 -0
- package/templates/database/sqlite/package.json +8 -0
- package/templates/database/sqlite/prisma/schema.prisma +48 -0
- package/templates/database/sqlite/src/database/drizzle.ts +11 -0
- package/templates/database/sqlite/src/database/schema.ts +52 -0
- package/templates/orm/drizzle/drizzle.config.ts +13 -0
- package/templates/orm/drizzle/package.json +90 -0
- package/templates/orm/drizzle/src/app.module.ts +73 -0
- package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
- package/templates/orm/drizzle/src/database/database.module.ts +25 -0
- package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
- package/templates/orm/drizzle/src/database/schema.ts +60 -0
- package/templates/orm/drizzle/src/database/seed.ts +87 -0
- package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
- package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
- package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
- package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
- package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
- package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
- package/templates/orm/mongoose/.env.example +22 -0
- package/templates/orm/mongoose/package.json +86 -0
- package/templates/orm/mongoose/src/app.module.ts +73 -0
- package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
- package/templates/orm/mongoose/src/database/database.module.ts +19 -0
- package/templates/orm/mongoose/src/database/seed.ts +107 -0
- package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
- package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
- package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
- package/templates/orm/mongoose/src/schemas/index.ts +2 -0
- package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
- package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
- package/templates/orm/prisma/package.json +102 -0
- package/templates/orm/prisma/prisma/schema.prisma +60 -0
- package/templates/orm/prisma/prisma/seed.ts +69 -0
- package/templates/orm/prisma/src/app.module.ts +55 -0
- package/templates/orm/prisma/src/config/env.validation.ts +56 -0
- package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
- package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
- package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
- package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
- package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
- package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
- package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
- package/templates/orm/typeorm/package.json +100 -0
- package/templates/orm/typeorm/src/app.module.ts +55 -0
- package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
- package/templates/orm/typeorm/src/database/data-source.ts +19 -0
- package/templates/orm/typeorm/src/database/database.module.ts +27 -0
- package/templates/orm/typeorm/src/database/seed.ts +76 -0
- package/templates/orm/typeorm/src/entities/index.ts +2 -0
- package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
- package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
- package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
- package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
- package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
- package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
- package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
- package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
- package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
- package/templates/swagger/src/main.ts +116 -0
- package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
- package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
- package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
- package/templates/swagger/src/modules/users/users.controller.ts +214 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
- 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,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 {}
|