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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/template/_claude/rules/server/project-conventions.md +64 -0
  3. package/template/server/.env.example +23 -0
  4. package/template/server/.env.example.production +21 -0
  5. package/template/server/package.json +1 -0
  6. package/template/server/postman/collection.json +415 -0
  7. package/template/server/postman/environment.json +31 -0
  8. package/template/server/src/app.ts +40 -10
  9. package/template/server/src/config/env.ts +9 -0
  10. package/template/server/src/config/rate-limit.config.ts +114 -0
  11. package/template/server/src/jobs/cleanup-deleted-accounts.job.ts +83 -0
  12. package/template/server/src/{libs/cleanup.ts → jobs/cleanup-expired-auth.job.ts} +10 -4
  13. package/template/server/src/jobs/index.ts +20 -0
  14. package/template/server/src/libs/__tests__/http.test.ts +414 -0
  15. package/template/server/src/libs/http.ts +66 -0
  16. package/template/server/src/libs/ip-block.ts +145 -0
  17. package/template/server/src/libs/storage/file-validator.ts +4 -3
  18. package/template/server/src/libs/storage/image-optimizer.service.ts +1 -1
  19. package/template/server/src/modules/admin/admin.controller.ts +41 -0
  20. package/template/server/src/modules/admin/admin.routes.ts +45 -0
  21. package/template/server/src/modules/auth/auth.routes.ts +10 -30
  22. package/template/server/src/modules/users/users.controller.ts +70 -1
  23. package/template/server/src/modules/users/users.repo.ts +27 -0
  24. package/template/server/src/modules/users/users.routes.ts +86 -16
  25. package/template/server/src/modules/users/users.schemas.ts +38 -0
  26. package/template/server/src/modules/users/users.service.ts +110 -2
  27. package/template/server/tsconfig.json +2 -1
  28. 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 { NotFoundError, BadRequestError } from '@shared/errors/errors.js';
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
@@ -19,7 +19,8 @@
19
19
  "@modules/*": ["./src/modules/*"],
20
20
  "@libs/*": ["./src/libs/*"],
21
21
  "@config/*": ["./src/config/*"],
22
- "@shared/*": ["./src/shared/*"]
22
+ "@shared/*": ["./src/shared/*"],
23
+ "@jobs/*": ["./src/jobs/*"]
23
24
  }
24
25
  },
25
26
  "include": ["src/**/*"],