create-tigra 2.2.0 → 2.3.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 (27) hide show
  1. package/bin/create-tigra.js +2 -0
  2. package/package.json +4 -1
  3. package/template/_claude/commands/create-client.md +1 -4
  4. package/template/_claude/commands/create-server.md +0 -1
  5. package/template/_claude/rules/client/01-project-structure.md +0 -3
  6. package/template/_claude/rules/client/03-data-and-state.md +1 -1
  7. package/template/_claude/rules/server/project-conventions.md +13 -0
  8. package/template/client/package.json +2 -1
  9. package/template/server/package.json +2 -1
  10. package/template/server/postman/collection.json +114 -5
  11. package/template/server/postman/environment.json +2 -2
  12. package/template/server/prisma/schema.prisma +17 -1
  13. package/template/server/src/app.ts +4 -1
  14. package/template/server/src/jobs/cleanup-deleted-accounts.job.ts +3 -6
  15. package/template/server/src/libs/auth.ts +45 -1
  16. package/template/server/src/libs/ip-block.ts +90 -29
  17. package/template/server/src/libs/requestLogger.ts +1 -1
  18. package/template/server/src/libs/storage/file-storage.service.ts +65 -18
  19. package/template/server/src/libs/storage/file-validator.ts +0 -8
  20. package/template/server/src/modules/admin/admin.controller.ts +4 -3
  21. package/template/server/src/modules/auth/auth.repo.ts +18 -0
  22. package/template/server/src/modules/auth/auth.service.ts +52 -26
  23. package/template/server/src/modules/users/users.controller.ts +39 -21
  24. package/template/server/src/modules/users/users.routes.ts +127 -6
  25. package/template/server/src/modules/users/users.schemas.ts +24 -4
  26. package/template/server/src/modules/users/users.service.ts +23 -10
  27. package/template/server/src/shared/types/index.ts +2 -0
@@ -7,14 +7,17 @@
7
7
  import { z } from 'zod';
8
8
 
9
9
  /**
10
- * Schema for getting a user's avatar
10
+ * Schema for :userId URL parameter
11
11
  *
12
- * Validates userId parameter in URL
12
+ * Reused across all routes that take :userId in the URL.
13
13
  */
14
- export const GetUserAvatarSchema = z.object({
14
+ export const UserIdParamSchema = z.object({
15
15
  userId: z.string().uuid({ message: 'Invalid user ID format' }),
16
16
  });
17
17
 
18
+ /** Alias for backward compatibility */
19
+ export const GetUserAvatarSchema = UserIdParamSchema;
20
+
18
21
  /**
19
22
  * Schema for updating user profile
20
23
  *
@@ -50,10 +53,27 @@ export const DeleteAccountSchema = z.object({
50
53
  password: z.string().min(1, 'Password is required'),
51
54
  });
52
55
 
56
+ /**
57
+ * Schema for admin-initiated password change
58
+ *
59
+ * Admin does not need to provide the user's current password.
60
+ */
61
+ export const AdminChangePasswordSchema = z.object({
62
+ newPassword: z
63
+ .string()
64
+ .min(8, 'Password must be at least 8 characters')
65
+ .max(128, 'Password must be at most 128 characters')
66
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
67
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
68
+ .regex(/[0-9]/, 'Password must contain at least one number'),
69
+ });
70
+
53
71
  /**
54
72
  * Type inference from schemas
55
73
  */
56
- export type GetUserAvatarParams = z.infer<typeof GetUserAvatarSchema>;
74
+ export type UserIdParams = z.infer<typeof UserIdParamSchema>;
75
+ export type GetUserAvatarParams = UserIdParams;
57
76
  export type UpdateProfileInput = z.infer<typeof UpdateProfileSchema>;
58
77
  export type ChangePasswordInput = z.infer<typeof ChangePasswordSchema>;
78
+ export type AdminChangePasswordInput = z.infer<typeof AdminChangePasswordSchema>;
59
79
  export type DeleteAccountInput = z.infer<typeof DeleteAccountSchema>;
@@ -14,7 +14,7 @@ import { verifyPassword, hashPassword } from '@libs/password.js';
14
14
  import { NotFoundError, BadRequestError, UnauthorizedError } from '@shared/errors/errors.js';
15
15
  import { logger } from '@libs/logger.js';
16
16
  import path from 'path';
17
- import type { UpdateProfileInput, ChangePasswordInput } from './users.schemas.js';
17
+ import type { UpdateProfileInput, ChangePasswordInput, AdminChangePasswordInput } from './users.schemas.js';
18
18
 
19
19
  /**
20
20
  * Users Service Class
@@ -144,7 +144,7 @@ class UsersService {
144
144
  }
145
145
 
146
146
  // Extract filename from URL (last segment)
147
- // URL format: /uploads/avatars/{userId}/{filename}
147
+ // URL format: /uploads/users/{userId}/avatar/{filename}
148
148
  const filename = path.basename(user.avatarUrl);
149
149
 
150
150
  // Build full file path
@@ -214,15 +214,22 @@ class UsersService {
214
214
  * @throws UnauthorizedError if current password is wrong
215
215
  * @throws BadRequestError if new password same as current
216
216
  */
217
- async changePassword(userId: string, input: ChangePasswordInput): Promise<void> {
217
+ async changePassword(
218
+ userId: string,
219
+ input: ChangePasswordInput | AdminChangePasswordInput,
220
+ skipPasswordVerification: boolean = false
221
+ ): Promise<void> {
218
222
  const user = await authRepo.findUserById(userId);
219
223
  if (!user) {
220
224
  throw new NotFoundError('User not found', 'USER_NOT_FOUND');
221
225
  }
222
226
 
223
- const isValid = await verifyPassword(input.currentPassword, user.password);
224
- if (!isValid) {
225
- throw new UnauthorizedError('Current password is incorrect', 'INVALID_CREDENTIALS');
227
+ if (!skipPasswordVerification) {
228
+ const fullInput = input as ChangePasswordInput;
229
+ const isValid = await verifyPassword(fullInput.currentPassword, user.password);
230
+ if (!isValid) {
231
+ throw new UnauthorizedError('Current password is incorrect', 'INVALID_CREDENTIALS');
232
+ }
226
233
  }
227
234
 
228
235
  const isSamePassword = await verifyPassword(input.newPassword, user.password);
@@ -250,15 +257,21 @@ class UsersService {
250
257
  * @throws NotFoundError if user not found
251
258
  * @throws UnauthorizedError if password is wrong
252
259
  */
253
- async deleteAccount(userId: string, password: string): Promise<void> {
260
+ async deleteAccount(
261
+ userId: string,
262
+ password: string,
263
+ skipPasswordVerification: boolean = false
264
+ ): Promise<void> {
254
265
  const user = await authRepo.findUserById(userId);
255
266
  if (!user) {
256
267
  throw new NotFoundError('User not found', 'USER_NOT_FOUND');
257
268
  }
258
269
 
259
- const isValid = await verifyPassword(password, user.password);
260
- if (!isValid) {
261
- throw new UnauthorizedError('Incorrect password', 'INVALID_CREDENTIALS');
270
+ if (!skipPasswordVerification) {
271
+ const isValid = await verifyPassword(password, user.password);
272
+ if (!isValid) {
273
+ throw new UnauthorizedError('Incorrect password', 'INVALID_CREDENTIALS');
274
+ }
262
275
  }
263
276
 
264
277
  logger.info({ msg: 'Soft-deleting user account', userId });
@@ -18,6 +18,8 @@ declare module 'fastify' {
18
18
  interface FastifyRequest {
19
19
  user: JwtPayload;
20
20
  startTime?: number; // Added for duration calculation
21
+ targetUserId?: string; // Set by resolveMe or resolveTargetUser middleware
22
+ isAdminAction?: boolean; // True when admin is acting on another user
21
23
  }
22
24
  }
23
25