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