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