create-tigra 2.1.4 → 2.2.0
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/package.json +1 -1
- package/template/_claude/rules/server/project-conventions.md +64 -0
- package/template/server/.env.example +23 -0
- package/template/server/.env.example.production +21 -0
- package/template/server/package.json +1 -0
- package/template/server/postman/collection.json +415 -0
- package/template/server/postman/environment.json +31 -0
- package/template/server/src/app.ts +40 -10
- package/template/server/src/config/env.ts +9 -0
- package/template/server/src/config/rate-limit.config.ts +114 -0
- package/template/server/src/jobs/cleanup-deleted-accounts.job.ts +83 -0
- package/template/server/src/{libs/cleanup.ts → jobs/cleanup-expired-auth.job.ts} +10 -4
- package/template/server/src/jobs/index.ts +20 -0
- package/template/server/src/libs/__tests__/http.test.ts +414 -0
- package/template/server/src/libs/http.ts +66 -0
- package/template/server/src/libs/ip-block.ts +145 -0
- package/template/server/src/libs/storage/file-validator.ts +4 -3
- package/template/server/src/libs/storage/image-optimizer.service.ts +1 -1
- package/template/server/src/modules/admin/admin.controller.ts +41 -0
- package/template/server/src/modules/admin/admin.routes.ts +45 -0
- package/template/server/src/modules/auth/auth.routes.ts +10 -30
- package/template/server/src/modules/users/users.controller.ts +70 -1
- package/template/server/src/modules/users/users.repo.ts +27 -0
- package/template/server/src/modules/users/users.routes.ts +86 -16
- package/template/server/src/modules/users/users.schemas.ts +38 -0
- package/template/server/src/modules/users/users.service.ts +110 -2
- package/template/server/tsconfig.json +2 -1
- package/template/server/uploads/avatars/.gitkeep +0 -1
|
@@ -15,7 +15,45 @@ export const GetUserAvatarSchema = z.object({
|
|
|
15
15
|
userId: z.string().uuid({ message: 'Invalid user ID format' }),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Schema for updating user profile
|
|
20
|
+
*
|
|
21
|
+
* All fields optional — at least one must be provided (validated in service layer)
|
|
22
|
+
*/
|
|
23
|
+
export const UpdateProfileSchema = z.object({
|
|
24
|
+
firstName: z.string().min(2, 'First name must be at least 2 characters').max(100).trim().optional(),
|
|
25
|
+
lastName: z.string().min(2, 'Last name must be at least 2 characters').max(100).trim().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Schema for changing user password
|
|
30
|
+
*
|
|
31
|
+
* Requires current password for verification and new password meeting strength rules
|
|
32
|
+
*/
|
|
33
|
+
export const ChangePasswordSchema = z.object({
|
|
34
|
+
currentPassword: z.string().min(1, 'Current password is required'),
|
|
35
|
+
newPassword: z
|
|
36
|
+
.string()
|
|
37
|
+
.min(8, 'Password must be at least 8 characters')
|
|
38
|
+
.max(128, 'Password must be at most 128 characters')
|
|
39
|
+
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
|
40
|
+
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
|
41
|
+
.regex(/[0-9]/, 'Password must contain at least one number'),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Schema for deleting user account
|
|
46
|
+
*
|
|
47
|
+
* Requires password confirmation for security
|
|
48
|
+
*/
|
|
49
|
+
export const DeleteAccountSchema = z.object({
|
|
50
|
+
password: z.string().min(1, 'Password is required'),
|
|
51
|
+
});
|
|
52
|
+
|
|
18
53
|
/**
|
|
19
54
|
* Type inference from schemas
|
|
20
55
|
*/
|
|
21
56
|
export type GetUserAvatarParams = z.infer<typeof GetUserAvatarSchema>;
|
|
57
|
+
export type UpdateProfileInput = z.infer<typeof UpdateProfileSchema>;
|
|
58
|
+
export type ChangePasswordInput = z.infer<typeof ChangePasswordSchema>;
|
|
59
|
+
export type DeleteAccountInput = z.infer<typeof DeleteAccountSchema>;
|
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import { usersRepository } from './users.repo.js';
|
|
8
8
|
import type { SafeUser } from './users.repo.js';
|
|
9
|
+
import * as authRepo from '@modules/auth/auth.repo.js';
|
|
10
|
+
import { sessionRepository } from '@modules/auth/session.repo.js';
|
|
9
11
|
import { fileStorageService } from '@libs/storage/file-storage.service.js';
|
|
10
12
|
import { imageOptimizerService } from '@libs/storage/image-optimizer.service.js';
|
|
11
|
-
import {
|
|
13
|
+
import { verifyPassword, hashPassword } from '@libs/password.js';
|
|
14
|
+
import { NotFoundError, BadRequestError, UnauthorizedError } from '@shared/errors/errors.js';
|
|
12
15
|
import { logger } from '@libs/logger.js';
|
|
13
16
|
import path from 'path';
|
|
17
|
+
import type { UpdateProfileInput, ChangePasswordInput } from './users.schemas.js';
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Users Service Class
|
|
17
21
|
*
|
|
18
|
-
* Handles business logic for user avatar operations.
|
|
22
|
+
* Handles business logic for user profile and avatar operations.
|
|
19
23
|
*/
|
|
20
24
|
class UsersService {
|
|
21
25
|
/**
|
|
@@ -163,6 +167,110 @@ class UsersService {
|
|
|
163
167
|
url: user.avatarUrl,
|
|
164
168
|
};
|
|
165
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Updates a user's profile
|
|
173
|
+
*
|
|
174
|
+
* @param userId - User's unique ID
|
|
175
|
+
* @param input - Fields to update (firstName, lastName)
|
|
176
|
+
* @returns Updated user
|
|
177
|
+
* @throws NotFoundError if user not found
|
|
178
|
+
* @throws BadRequestError if no fields provided
|
|
179
|
+
*/
|
|
180
|
+
async updateProfile(userId: string, input: UpdateProfileInput): Promise<SafeUser> {
|
|
181
|
+
const { firstName, lastName } = input;
|
|
182
|
+
if (!firstName && !lastName) {
|
|
183
|
+
throw new BadRequestError('At least one field must be provided', 'INVALID_INPUT');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const user = await usersRepository.getUserById(userId);
|
|
187
|
+
if (!user) {
|
|
188
|
+
throw new NotFoundError('User not found', 'USER_NOT_FOUND');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
logger.info({
|
|
192
|
+
msg: 'Updating user profile',
|
|
193
|
+
userId,
|
|
194
|
+
fields: Object.keys(input).filter(k => input[k as keyof UpdateProfileInput] !== undefined),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const updateData: { firstName?: string; lastName?: string } = {};
|
|
198
|
+
if (firstName) updateData.firstName = firstName;
|
|
199
|
+
if (lastName) updateData.lastName = lastName;
|
|
200
|
+
|
|
201
|
+
const updatedUser = await usersRepository.updateUserProfile(userId, updateData);
|
|
202
|
+
|
|
203
|
+
logger.info({ msg: 'User profile updated', userId });
|
|
204
|
+
|
|
205
|
+
return updatedUser;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Changes a user's password
|
|
210
|
+
*
|
|
211
|
+
* @param userId - User's unique ID
|
|
212
|
+
* @param input - Current and new password
|
|
213
|
+
* @throws NotFoundError if user not found
|
|
214
|
+
* @throws UnauthorizedError if current password is wrong
|
|
215
|
+
* @throws BadRequestError if new password same as current
|
|
216
|
+
*/
|
|
217
|
+
async changePassword(userId: string, input: ChangePasswordInput): Promise<void> {
|
|
218
|
+
const user = await authRepo.findUserById(userId);
|
|
219
|
+
if (!user) {
|
|
220
|
+
throw new NotFoundError('User not found', 'USER_NOT_FOUND');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const isValid = await verifyPassword(input.currentPassword, user.password);
|
|
224
|
+
if (!isValid) {
|
|
225
|
+
throw new UnauthorizedError('Current password is incorrect', 'INVALID_CREDENTIALS');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const isSamePassword = await verifyPassword(input.newPassword, user.password);
|
|
229
|
+
if (isSamePassword) {
|
|
230
|
+
throw new BadRequestError('New password must be different from current password', 'SAME_PASSWORD');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const hashedPassword = await hashPassword(input.newPassword);
|
|
234
|
+
await authRepo.updateUserPassword(userId, hashedPassword);
|
|
235
|
+
|
|
236
|
+
logger.info({ msg: 'User password changed', userId });
|
|
237
|
+
|
|
238
|
+
// Security: invalidate all sessions and refresh tokens
|
|
239
|
+
await sessionRepository.deleteAllUserSessions(userId);
|
|
240
|
+
await authRepo.deleteRefreshTokensByUserId(userId);
|
|
241
|
+
|
|
242
|
+
logger.info({ msg: 'All sessions invalidated after password change', userId });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Soft-deletes a user account
|
|
247
|
+
*
|
|
248
|
+
* @param userId - User's unique ID
|
|
249
|
+
* @param password - Password confirmation
|
|
250
|
+
* @throws NotFoundError if user not found
|
|
251
|
+
* @throws UnauthorizedError if password is wrong
|
|
252
|
+
*/
|
|
253
|
+
async deleteAccount(userId: string, password: string): Promise<void> {
|
|
254
|
+
const user = await authRepo.findUserById(userId);
|
|
255
|
+
if (!user) {
|
|
256
|
+
throw new NotFoundError('User not found', 'USER_NOT_FOUND');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const isValid = await verifyPassword(password, user.password);
|
|
260
|
+
if (!isValid) {
|
|
261
|
+
throw new UnauthorizedError('Incorrect password', 'INVALID_CREDENTIALS');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
logger.info({ msg: 'Soft-deleting user account', userId });
|
|
265
|
+
|
|
266
|
+
await usersRepository.softDeleteUser(userId);
|
|
267
|
+
|
|
268
|
+
// Delete all sessions and refresh tokens
|
|
269
|
+
await sessionRepository.deleteAllUserSessions(userId);
|
|
270
|
+
await authRepo.deleteRefreshTokensByUserId(userId);
|
|
271
|
+
|
|
272
|
+
logger.info({ msg: 'User account deleted', userId });
|
|
273
|
+
}
|
|
166
274
|
}
|
|
167
275
|
|
|
168
276
|
// Export singleton instance
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|