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.
- package/bin/create-tigra.js +2 -0
- package/package.json +4 -1
- package/template/_claude/commands/create-client.md +1 -4
- package/template/_claude/commands/create-server.md +0 -1
- package/template/_claude/rules/client/01-project-structure.md +0 -3
- package/template/_claude/rules/client/03-data-and-state.md +1 -1
- package/template/_claude/rules/server/project-conventions.md +13 -0
- package/template/client/package.json +2 -1
- package/template/server/package.json +2 -1
- package/template/server/postman/collection.json +114 -5
- package/template/server/postman/environment.json +2 -2
- package/template/server/prisma/schema.prisma +17 -1
- package/template/server/src/app.ts +4 -1
- package/template/server/src/jobs/cleanup-deleted-accounts.job.ts +3 -6
- package/template/server/src/libs/auth.ts +45 -1
- package/template/server/src/libs/ip-block.ts +90 -29
- package/template/server/src/libs/requestLogger.ts +1 -1
- package/template/server/src/libs/storage/file-storage.service.ts +65 -18
- package/template/server/src/libs/storage/file-validator.ts +0 -8
- package/template/server/src/modules/admin/admin.controller.ts +4 -3
- package/template/server/src/modules/auth/auth.repo.ts +18 -0
- package/template/server/src/modules/auth/auth.service.ts +52 -26
- package/template/server/src/modules/users/users.controller.ts +39 -21
- package/template/server/src/modules/users/users.routes.ts +127 -6
- package/template/server/src/modules/users/users.schemas.ts +24 -4
- package/template/server/src/modules/users/users.service.ts +23 -10
- 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
|
|
10
|
+
* Schema for :userId URL parameter
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Reused across all routes that take :userId in the URL.
|
|
13
13
|
*/
|
|
14
|
-
export const
|
|
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
|
|
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/
|
|
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(
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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(
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|