create-tigra 3.0.3 → 3.1.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/README.md +8 -2
- package/bin/create-tigra.js +5 -19
- package/modules/email-verification/client/hooks/useVerification.ts +3 -3
- package/package.json +1 -1
- package/template/.agents/skills/security-audit/AI-AND-LLM.md +83 -0
- package/template/.agents/skills/security-audit/ATTACK-CLASSES.md +130 -0
- package/template/.agents/skills/security-audit/CLIENT-SIDE.md +83 -0
- package/template/.agents/skills/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/template/.agents/skills/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/template/.agents/skills/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/template/.agents/skills/security-audit/HUNTING.md +251 -0
- package/template/.agents/skills/security-audit/LICENSE +21 -0
- package/template/.agents/skills/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/template/.agents/skills/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/template/.agents/skills/security-audit/RECONNAISSANCE.md +156 -0
- package/template/.agents/skills/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/template/.agents/skills/security-audit/SKILL.md +192 -0
- package/template/.agents/skills/security-audit/SOURCE.md +5 -0
- package/template/.agents/skills/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/template/.agents/skills/security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/template/.agents/skills/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/template/.agents/skills/security-audit/report-schema.json +461 -0
- package/template/.agents/skills/security-audit/validate-coverage-ledger.cjs +872 -0
- package/template/.agents/skills/security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/template/.agents/skills/security-audit/validate-findings.cjs +773 -0
- package/template/.agents/skills/security-audit/validate-findings.test.cjs +652 -0
- package/template/AGENTS.md +45 -0
- package/template/client/AGENTS.md +22 -0
- package/template/client/Dockerfile +11 -1
- package/template/client/package-lock.json +410 -324
- package/template/client/package.json +4 -4
- package/template/client/scripts/next-dev.cjs +47 -0
- package/template/client/src/app/(auth)/layout.tsx +9 -0
- package/template/client/src/app/(auth)/loading.tsx +7 -0
- package/template/client/src/app/(main)/layout.tsx +11 -0
- package/template/client/src/app/(main)/loading.tsx +7 -0
- package/template/client/src/app/globals.css +4 -0
- package/template/client/src/app/loading.tsx +2 -6
- package/template/client/src/app/not-found.tsx +2 -3
- package/template/client/src/app/providers.tsx +6 -3
- package/template/client/src/components/common/AppLink.tsx +84 -0
- package/template/client/src/components/common/EmptyState.tsx +2 -2
- package/template/client/src/components/common/Pagination.tsx +3 -2
- package/template/client/src/components/common/RouteLoadingShell.tsx +21 -0
- package/template/client/src/components/common/SmoothNavigationProvider.tsx +151 -0
- package/template/client/src/components/layout/Header.tsx +12 -12
- package/template/client/src/features/admin/hooks/useAdminSessions.ts +2 -2
- package/template/client/src/features/admin/hooks/useAdminUsers.ts +3 -3
- package/template/client/src/features/auth/components/AuthInitializer.tsx +3 -2
- package/template/client/src/features/auth/components/LoginForm.tsx +3 -3
- package/template/client/src/features/auth/components/RegisterForm.tsx +3 -3
- package/template/client/src/features/auth/hooks/useAuth.ts +2 -2
- package/template/client/src/features/auth/hooks/usePasswordReset.ts +2 -2
- package/template/client/src/hooks/useAppRouter.ts +40 -0
- package/template/client/src/instrumentation-client.ts +29 -21
- package/template/client/src/instrumentation.ts +21 -17
- package/template/client/src/styles/themes/default.css +1 -1
- package/template/gitignore +0 -6
- package/template/server/AGENTS.md +28 -0
- package/template/server/Dockerfile +9 -3
- package/template/server/package-lock.json +671 -522
- package/template/server/package.json +8 -8
- package/template/server/src/modules/auth/__tests__/auth.service.test.ts +25 -41
- package/template/server/src/modules/auth/auth.repo.ts +0 -12
- package/template/server/src/modules/auth/auth.service.ts +29 -57
- package/template/_claude/QUICK_REFERENCE.md +0 -193
- package/template/_claude/README.md +0 -53
- package/template/_claude/commands/create-client.md +0 -878
- package/template/_claude/commands/create-server.md +0 -388
- package/template/_claude/hooks/restrict-paths.sh +0 -51
- package/template/_claude/rules/client/01-project-structure.md +0 -147
- package/template/_claude/rules/client/02-components-and-types.md +0 -146
- package/template/_claude/rules/client/03-data-and-state.md +0 -195
- package/template/_claude/rules/client/04-design-system.md +0 -408
- package/template/_claude/rules/client/05-security.md +0 -55
- package/template/_claude/rules/client/06-ux-checklist.md +0 -111
- package/template/_claude/rules/client/07-deployment.md +0 -99
- package/template/_claude/rules/client/08-lockfile-cross-platform.md +0 -79
- package/template/_claude/rules/client/core.md +0 -46
- package/template/_claude/rules/global/completion-reports.md +0 -178
- package/template/_claude/rules/global/core.md +0 -104
- package/template/_claude/rules/global/investigation-before-conclusions.md +0 -57
- package/template/_claude/rules/server/core.md +0 -52
- package/template/_claude/rules/server/database.md +0 -124
- package/template/_claude/rules/server/deployment.md +0 -78
- package/template/_claude/rules/server/project-conventions.md +0 -254
- package/template/_claude/rules/server/response-handling.md +0 -144
- package/template/_claude/settings.json +0 -15
- package/template/_claude/skills/clean-ui/SKILL.md +0 -63
- package/template/_claude/skills/role/SKILL.md +0 -39
- package/template/_claude/skills/theme/SKILL.md +0 -109
|
@@ -39,20 +39,20 @@
|
|
|
39
39
|
"@fastify/jwt": "^10.1.0",
|
|
40
40
|
"@fastify/multipart": "^9.0.2",
|
|
41
41
|
"@fastify/rate-limit": "^10.3.0",
|
|
42
|
-
"@fastify/static": "^
|
|
42
|
+
"@fastify/static": "^10.1.4",
|
|
43
43
|
"@prisma/client": "^6.19.3",
|
|
44
44
|
"@sentry/node": "^10.62.0",
|
|
45
45
|
"argon2": "^0.44.0",
|
|
46
|
-
"axios": "^1.
|
|
46
|
+
"axios": "^1.20.0",
|
|
47
47
|
"dotenv": "^16.4.7",
|
|
48
|
-
"fastify": "^5.
|
|
48
|
+
"fastify": "^5.12.5",
|
|
49
49
|
"fastify-type-provider-zod": "^6.1.0",
|
|
50
50
|
"ioredis": "^5.9.2",
|
|
51
51
|
"pino": "^10.3.1",
|
|
52
52
|
"pino-pretty": "^13.1.3",
|
|
53
53
|
"resend": "^6.9.4",
|
|
54
|
-
"sharp": "^0.
|
|
55
|
-
"uuid": "^
|
|
54
|
+
"sharp": "^0.35.4",
|
|
55
|
+
"uuid": "^14.0.2",
|
|
56
56
|
"zod": "^4.3.6"
|
|
57
57
|
},
|
|
58
58
|
"overrides": {
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"@testcontainers/redis": "^12.0.3",
|
|
69
69
|
"@types/node": "^20.17.10",
|
|
70
70
|
"@types/uuid": "^10.0.0",
|
|
71
|
-
"@vitest/coverage-v8": "^4.
|
|
72
|
-
"@vitest/ui": "^4.
|
|
71
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
72
|
+
"@vitest/ui": "^4.1.11",
|
|
73
73
|
"eslint": "^10.0.1",
|
|
74
74
|
"prisma": "^6.19.3",
|
|
75
75
|
"testcontainers": "^12.0.3",
|
|
@@ -77,6 +77,6 @@
|
|
|
77
77
|
"tsx": "^4.21.0",
|
|
78
78
|
"typescript": "^5.9.3",
|
|
79
79
|
"typescript-eslint": "^8.55.0",
|
|
80
|
-
"vitest": "^4.
|
|
80
|
+
"vitest": "^4.1.11"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -137,6 +137,9 @@ describe('Auth Service', () => {
|
|
|
137
137
|
|
|
138
138
|
// Act & Assert
|
|
139
139
|
await expect(authService.register(validRegisterInput)).rejects.toThrow(ConflictError);
|
|
140
|
+
await expect(authService.register(validRegisterInput)).rejects.toThrow(
|
|
141
|
+
'An account with this email was recently deleted and cannot be registered yet.',
|
|
142
|
+
);
|
|
140
143
|
expect(authRepo.createUser).not.toHaveBeenCalled();
|
|
141
144
|
});
|
|
142
145
|
});
|
|
@@ -193,12 +196,33 @@ describe('Auth Service', () => {
|
|
|
193
196
|
it('should throw UnauthorizedError if user not found', async () => {
|
|
194
197
|
// Arrange
|
|
195
198
|
vi.mocked(authRepo.findUserByEmail).mockResolvedValue(null);
|
|
196
|
-
vi.mocked(authRepo.findDeletedUserByEmail).mockResolvedValue(null);
|
|
197
199
|
|
|
198
200
|
// Act & Assert
|
|
199
201
|
await expect(authService.login(validLoginInput)).rejects.toThrow(UnauthorizedError);
|
|
200
202
|
await expect(authService.login(validLoginInput)).rejects.toThrow('Invalid email or password');
|
|
203
|
+
expect(authRepo.findDeletedUserByEmail).not.toHaveBeenCalled();
|
|
204
|
+
expect(verifyPassword).not.toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should not restore or authenticate a soft-deleted account with correct credentials', async () => {
|
|
208
|
+
// findUserByEmail excludes soft-deleted records, so login must stop before
|
|
209
|
+
// password verification, token creation, or session creation.
|
|
210
|
+
vi.mocked(authRepo.findUserByEmail).mockResolvedValue(null);
|
|
211
|
+
vi.mocked(authRepo.findDeletedUserByEmail).mockResolvedValue({
|
|
212
|
+
...testUsers.validUser,
|
|
213
|
+
deletedAt: new Date('2024-02-01T00:00:00Z'),
|
|
214
|
+
isActive: false,
|
|
215
|
+
});
|
|
216
|
+
vi.mocked(verifyPassword).mockResolvedValue(true);
|
|
217
|
+
|
|
218
|
+
await expect(authService.login(validLoginInput)).rejects.toThrow(UnauthorizedError);
|
|
219
|
+
await expect(authService.login(validLoginInput)).rejects.toThrow('Invalid email or password');
|
|
220
|
+
expect(authRepo.findDeletedUserByEmail).not.toHaveBeenCalled();
|
|
201
221
|
expect(verifyPassword).not.toHaveBeenCalled();
|
|
222
|
+
expect(authLib.signAccessToken).not.toHaveBeenCalled();
|
|
223
|
+
expect(authLib.generateRefreshToken).not.toHaveBeenCalled();
|
|
224
|
+
expect(sessionRepository.createSession).not.toHaveBeenCalled();
|
|
225
|
+
expect(authRepo.createRefreshToken).not.toHaveBeenCalled();
|
|
202
226
|
});
|
|
203
227
|
|
|
204
228
|
it('should throw ForbiddenError if account is not activated', async () => {
|
|
@@ -325,46 +349,6 @@ describe('Auth Service', () => {
|
|
|
325
349
|
});
|
|
326
350
|
});
|
|
327
351
|
|
|
328
|
-
describe('login — soft-deleted account restore path (lockout)', () => {
|
|
329
|
-
const loginInput = { email: 'test@example.com', password: 'WrongPassword!' };
|
|
330
|
-
const attackerIp = '203.0.113.7';
|
|
331
|
-
const softDeletedUser = {
|
|
332
|
-
...testUsers.validUser,
|
|
333
|
-
deletedAt: new Date('2024-02-01T00:00:00Z'),
|
|
334
|
-
isActive: false,
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
it('should record a failed attempt against the email+IP pair on wrong password for a soft-deleted account', async () => {
|
|
338
|
-
// Arrange
|
|
339
|
-
vi.mocked(authRepo.findUserByEmail).mockResolvedValue(null);
|
|
340
|
-
vi.mocked(authRepo.findDeletedUserByEmail).mockResolvedValue(softDeletedUser);
|
|
341
|
-
vi.mocked(verifyPassword).mockResolvedValue(false);
|
|
342
|
-
mockRedis.incr.mockResolvedValue(3); // below the first lockout threshold
|
|
343
|
-
|
|
344
|
-
// Act & Assert
|
|
345
|
-
await expect(authService.login(loginInput, 'test-agent', attackerIp)).rejects.toThrow(
|
|
346
|
-
'Invalid email or password',
|
|
347
|
-
);
|
|
348
|
-
expect(mockRedis.incr).toHaveBeenCalledWith(`login-fail:test@example.com:${attackerIp}`);
|
|
349
|
-
expect(authRepo.restoreUser).not.toHaveBeenCalled();
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('should reject a locked email+IP pair on the soft-delete path before verifying the password', async () => {
|
|
353
|
-
// Arrange
|
|
354
|
-
vi.mocked(authRepo.findUserByEmail).mockResolvedValue(null);
|
|
355
|
-
vi.mocked(authRepo.findDeletedUserByEmail).mockResolvedValue(softDeletedUser);
|
|
356
|
-
mockRedis.exists.mockResolvedValue(1); // lock key present for this pair
|
|
357
|
-
|
|
358
|
-
// Act & Assert
|
|
359
|
-
await expect(authService.login(loginInput, 'test-agent', attackerIp)).rejects.toThrow(
|
|
360
|
-
'Invalid email or password',
|
|
361
|
-
);
|
|
362
|
-
expect(mockRedis.exists).toHaveBeenCalledWith(`login-lock:test@example.com:${attackerIp}`);
|
|
363
|
-
expect(verifyPassword).not.toHaveBeenCalled();
|
|
364
|
-
expect(authRepo.restoreUser).not.toHaveBeenCalled();
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
352
|
describe('refresh', () => {
|
|
369
353
|
const validRefreshToken = 'valid-refresh-token';
|
|
370
354
|
|
|
@@ -13,18 +13,6 @@ export async function findDeletedUserByEmail(email: string): Promise<User | null
|
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export async function restoreUser(userId: string): Promise<void> {
|
|
17
|
-
await prisma.user.update({
|
|
18
|
-
where: { id: userId },
|
|
19
|
-
data: {
|
|
20
|
-
deletedAt: null,
|
|
21
|
-
isActive: true,
|
|
22
|
-
failedLoginAttempts: 0,
|
|
23
|
-
lockedUntil: null,
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
16
|
export async function findUserById(id: string): Promise<User | null> {
|
|
29
17
|
return prisma.user.findUnique({
|
|
30
18
|
where: { id, deletedAt: null },
|
|
@@ -157,11 +157,11 @@ export async function register(
|
|
|
157
157
|
throw new ConflictError('Email already registered', 'EMAIL_ALREADY_EXISTS');
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
//
|
|
160
|
+
// Keep the email reserved while the soft-deleted account is retained.
|
|
161
161
|
const deletedUser = await authRepo.findDeletedUserByEmail(input.email);
|
|
162
162
|
if (deletedUser) {
|
|
163
163
|
throw new ConflictError(
|
|
164
|
-
'An account with this email was recently deleted
|
|
164
|
+
'An account with this email was recently deleted and cannot be registered yet.',
|
|
165
165
|
'EMAIL_ALREADY_EXISTS',
|
|
166
166
|
);
|
|
167
167
|
}
|
|
@@ -219,68 +219,40 @@ export async function login(
|
|
|
219
219
|
deviceInfo?: string,
|
|
220
220
|
ipAddress?: string,
|
|
221
221
|
): Promise<AuthResult> {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// If no active user found, check for a soft-deleted account that can be restored
|
|
222
|
+
const user = await authRepo.findUserByEmail(input.email);
|
|
225
223
|
if (!user) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Same email+IP lockout as the normal path — the restore flow must not be
|
|
232
|
-
// a brute-force side door around the lockout.
|
|
233
|
-
if (await isLoginLocked(input.email, ipAddress)) {
|
|
234
|
-
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Verify password before restoring — don't restore on wrong password
|
|
238
|
-
const validPassword = await verifyPassword(input.password, deletedUser.password);
|
|
239
|
-
if (!validPassword) {
|
|
240
|
-
await recordFailedLogin(input.email, ipAddress);
|
|
241
|
-
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Successful restore-login — clear the email+IP failure counter, same as
|
|
245
|
-
// the normal path.
|
|
246
|
-
await clearFailedLogins(input.email, ipAddress);
|
|
247
|
-
|
|
248
|
-
// Restore account: clears deletedAt, sets isActive = true, resets lockout
|
|
249
|
-
await authRepo.restoreUser(deletedUser.id);
|
|
250
|
-
user = { ...deletedUser, deletedAt: null, isActive: true, failedLoginAttempts: 0, lockedUntil: null };
|
|
251
|
-
} else {
|
|
252
|
-
// Normal login flow for active accounts
|
|
224
|
+
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
225
|
+
}
|
|
253
226
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
227
|
+
// Distinct error for inactive accounts so the client can show proper messaging
|
|
228
|
+
if (!user.isActive) {
|
|
229
|
+
throw new ForbiddenError('Account is not activated. Please verify your account.', 'ACCOUNT_NOT_ACTIVE');
|
|
230
|
+
}
|
|
258
231
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
232
|
+
// Check account lockout — scoped to this email+IP pair (Redis), so an
|
|
233
|
+
// attacker spamming bad passwords only locks out their OWN address and
|
|
234
|
+
// cannot remotely lock the real user out (lockout DoS).
|
|
235
|
+
if (await isLoginLocked(input.email, ipAddress)) {
|
|
236
|
+
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
237
|
+
}
|
|
265
238
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
239
|
+
// DB-level lock (legacy data or manual admin lock) is still honored.
|
|
240
|
+
if (user.lockedUntil && user.lockedUntil > new Date()) {
|
|
241
|
+
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
242
|
+
}
|
|
270
243
|
|
|
271
|
-
|
|
244
|
+
const valid = await verifyPassword(input.password, user.password);
|
|
272
245
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
246
|
+
if (!valid) {
|
|
247
|
+
await recordFailedLogin(input.email, ipAddress);
|
|
248
|
+
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
249
|
+
}
|
|
277
250
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
251
|
+
// Successful login — clear the email+IP failure counter and any stale
|
|
252
|
+
// DB-level lockout state from before lockout moved to Redis.
|
|
253
|
+
await clearFailedLogins(input.email, ipAddress);
|
|
254
|
+
if (user.failedLoginAttempts > 0 || user.lockedUntil) {
|
|
255
|
+
await authRepo.resetFailedAttempts(user.id);
|
|
284
256
|
}
|
|
285
257
|
|
|
286
258
|
const accessToken = signAccessToken({
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
# Quick Reference for AI Assistants
|
|
2
|
-
|
|
3
|
-
**Read this first, then dive into specific rule files as needed.**
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
If project is empty start server/client commands (`/create-server`, `/create-client`)
|
|
8
|
-
|
|
9
|
-
## How Rules Are Organized
|
|
10
|
-
|
|
11
|
-
Rules are scoped by directory:
|
|
12
|
-
|
|
13
|
-
| Scope | Path | Applies To |
|
|
14
|
-
|-------|------|------------|
|
|
15
|
-
| **Global** | `.claude/rules/global/` | Entire workspace (server + client) |
|
|
16
|
-
| **Server** | `.claude/rules/server/` | Backend (Fastify API server) only |
|
|
17
|
-
| **Client** | `.claude/rules/client/` | Frontend (Next.js App Router) only |
|
|
18
|
-
|
|
19
|
-
Always check which scope you're working in before writing code.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Global Rules (Always Apply)
|
|
24
|
-
|
|
25
|
-
### Safe Editing
|
|
26
|
-
- Keep changes **small and focused**
|
|
27
|
-
- Preserve existing function signatures and exports unless explicitly asked
|
|
28
|
-
- Extend modules, don't rewrite
|
|
29
|
-
- Add `// TODO:` comments for ambiguities or follow-ups
|
|
30
|
-
- Never leave half-implemented features without explanation
|
|
31
|
-
- No noisy debug logs; mark temporary ones with `// TODO: remove debug log`
|
|
32
|
-
- Never delete or radically restructure large parts of the codebase
|
|
33
|
-
- Preserve existing behavior for critical flows (auth, payments, core business logic)
|
|
34
|
-
|
|
35
|
-
See: `global/core.md`
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## Server Rules Summary
|
|
40
|
-
|
|
41
|
-
### Architecture
|
|
42
|
-
```
|
|
43
|
-
Request -> Routes -> Controller -> Service -> Repository -> Database
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
- **Controllers**: Validate input (Zod), call services, return responses, throw typed errors. NO business logic, NO direct DB access.
|
|
47
|
-
- **Services**: All business logic. Throw typed `AppError` instances. NO HTTP concepts (request/reply). Return data or throw.
|
|
48
|
-
- **Repositories**: Database queries only.
|
|
49
|
-
|
|
50
|
-
### Module Structure
|
|
51
|
-
```
|
|
52
|
-
src/modules/<domain>/
|
|
53
|
-
<domain>.routes.ts
|
|
54
|
-
<domain>.controller.ts
|
|
55
|
-
<domain>.service.ts
|
|
56
|
-
<domain>.repo.ts
|
|
57
|
-
<domain>.schemas.ts
|
|
58
|
-
<domain>.types.ts (optional)
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### API Response Format (Mandatory)
|
|
62
|
-
```json
|
|
63
|
-
// Success
|
|
64
|
-
{ "success": true, "message": "...", "data": { ... } }
|
|
65
|
-
|
|
66
|
-
// Error
|
|
67
|
-
{ "success": false, "error": { "code": "ERROR_CODE", "message": "..." } }
|
|
68
|
-
```
|
|
69
|
-
- Use `successResponse()` / `paginatedResponse()` helpers
|
|
70
|
-
- Throw typed errors only (`AppError` subclasses)
|
|
71
|
-
- Global error handler formats all errors
|
|
72
|
-
|
|
73
|
-
### Auth Architecture
|
|
74
|
-
- **httpOnly cookies** for token storage (access_token + refresh_token)
|
|
75
|
-
- Account lockout with progressive thresholds
|
|
76
|
-
- Session tracking with device info and IP
|
|
77
|
-
- Transparent password rehash (bcrypt legacy -> argon2id)
|
|
78
|
-
|
|
79
|
-
### Database
|
|
80
|
-
- Schema changes via Prisma migrations only
|
|
81
|
-
- Development: `prisma:reset` freely; Production: `prisma:migrate deploy` only
|
|
82
|
-
- Prisma models: `PascalCase`; Fields: `camelCase`
|
|
83
|
-
- All main tables need: `id`, `createdAt`, `updatedAt`
|
|
84
|
-
|
|
85
|
-
### Code Style
|
|
86
|
-
- TypeScript strict mode, type all params and returns
|
|
87
|
-
- `async/await` over `.then()`
|
|
88
|
-
- Named exports (exception: `app.ts` default-exports `buildApp`)
|
|
89
|
-
- Use `logger` from `src/libs/`, never `console.log`
|
|
90
|
-
- Detect package manager from lockfile
|
|
91
|
-
|
|
92
|
-
See: `server/core.md`, `server/project-conventions.md`, `server/response-handling.md`, `server/database.md`
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
## Client Rules Summary
|
|
97
|
-
|
|
98
|
-
### Architecture
|
|
99
|
-
- Next.js App Router with Server Components by default
|
|
100
|
-
- Client Components only when interactivity is needed (`'use client'`)
|
|
101
|
-
- Feature modules under `src/features/<domain>/`
|
|
102
|
-
|
|
103
|
-
### Auth Flow
|
|
104
|
-
- **httpOnly cookies** — no tokens stored in Redux or localStorage
|
|
105
|
-
- `AuthInitializer` component hydrates user state on page load via `getMe()` API call
|
|
106
|
-
- Redux auth state: `{ user, isAuthenticated, isInitializing, isLoggingOut }`
|
|
107
|
-
- Axios `withCredentials: true` sends cookies automatically
|
|
108
|
-
|
|
109
|
-
### Module Structure
|
|
110
|
-
```
|
|
111
|
-
src/features/<domain>/
|
|
112
|
-
components/
|
|
113
|
-
hooks/
|
|
114
|
-
services/
|
|
115
|
-
store/ (Redux, if needed)
|
|
116
|
-
types/
|
|
117
|
-
actions/ (Server Actions, optional)
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### State Management
|
|
121
|
-
| State Type | Tool |
|
|
122
|
-
|------------|------|
|
|
123
|
-
| Server data (SSR) | Server Components |
|
|
124
|
-
| Server data (client) | React Query |
|
|
125
|
-
| Global client state | Redux (auth only) |
|
|
126
|
-
| Local state | useState / useReducer |
|
|
127
|
-
| URL state | useSearchParams |
|
|
128
|
-
|
|
129
|
-
### Component Rules
|
|
130
|
-
- Max 250 lines per component
|
|
131
|
-
- Max 5 props (use object if more)
|
|
132
|
-
- Max 3 levels of JSX nesting
|
|
133
|
-
- Use `cn()` for conditional classes
|
|
134
|
-
- Use Next.js `Image` and `Link` components
|
|
135
|
-
- Follow import order: React/Next -> third-party -> UI -> local -> hooks -> services -> types -> utils
|
|
136
|
-
|
|
137
|
-
### Styling
|
|
138
|
-
- Tailwind CSS v4 only, no inline styles
|
|
139
|
-
- OKLCH color space via CSS custom properties
|
|
140
|
-
- Use semantic color tokens (e.g., `bg-primary`, `text-foreground`)
|
|
141
|
-
- Never hardcode hex/rgb values
|
|
142
|
-
- Pair backgrounds with foregrounds for contrast
|
|
143
|
-
|
|
144
|
-
### Forms
|
|
145
|
-
- React Hook Form + Zod for complex forms
|
|
146
|
-
- Server Actions for simple forms
|
|
147
|
-
- Always validate client-side AND server-side
|
|
148
|
-
|
|
149
|
-
### Security
|
|
150
|
-
- Never inject raw HTML without sanitization (use DOMPurify)
|
|
151
|
-
- Never prefix secrets with `NEXT_PUBLIC_`
|
|
152
|
-
- Validate all inputs, sanitize all outputs
|
|
153
|
-
- Secure external links with `rel="noopener noreferrer"`
|
|
154
|
-
|
|
155
|
-
See: `client/core.md`, `client/01-project-structure.md` through `client/06-ux-checklist.md`
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## When Implementing Features
|
|
160
|
-
|
|
161
|
-
1. **Identify scope** - Are you working in server, client, or both?
|
|
162
|
-
2. **Read relevant rules** - Check the scoped rule files for that directory
|
|
163
|
-
3. **Summarize** what needs to be done
|
|
164
|
-
4. **List** files to create/modify
|
|
165
|
-
5. **Provide** complete code for each file
|
|
166
|
-
6. **Mention** migrations, env variables, or dependencies needed
|
|
167
|
-
7. **State assumptions** if unsure
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
## Full Documentation Index
|
|
172
|
-
|
|
173
|
-
### Global
|
|
174
|
-
- `global/core.md` - Safe editing, TypeScript, git, env, testing rules
|
|
175
|
-
|
|
176
|
-
### Server
|
|
177
|
-
- `server/core.md` - Index and non-negotiables
|
|
178
|
-
- `server/project-conventions.md` - Stack, folder structure, coding style, Postman
|
|
179
|
-
- `server/response-handling.md` - Response contract, error classes
|
|
180
|
-
- `server/database.md` - Database standards, migrations, indexing
|
|
181
|
-
|
|
182
|
-
### Client
|
|
183
|
-
- `client/core.md` - Architecture and non-negotiables
|
|
184
|
-
- `client/01-project-structure.md` - Folder structure, naming, imports, constants
|
|
185
|
-
- `client/02-components-and-types.md` - Component rules, TypeScript, API types
|
|
186
|
-
- `client/03-data-and-state.md` - State management, React Query, Redux, Axios, forms
|
|
187
|
-
- `client/04-design-system.md` - Colors, typography, spacing, motion, dark mode
|
|
188
|
-
- `client/05-security.md` - Token storage, env vars, CSP, validation
|
|
189
|
-
- `client/06-ux-checklist.md` - Cognitive load, accessibility, performance
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
**Last Updated**: 2026-02-20
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Claude Assistant Rules
|
|
2
|
-
|
|
3
|
-
This directory contains project-specific rules and guidelines for Claude.
|
|
4
|
-
|
|
5
|
-
## Directory Structure
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
.claude/
|
|
9
|
-
├── README.md # This file
|
|
10
|
-
└── rules/
|
|
11
|
-
├── global/ # Cross-cutting rules (always active)
|
|
12
|
-
│ └── core.md # Safe editing, TypeScript, git, env, testing
|
|
13
|
-
├── client/ # Next.js App Router rules
|
|
14
|
-
│ ├── core.md # Index — which file to read for what
|
|
15
|
-
│ ├── 01-project-structure.md
|
|
16
|
-
│ ├── 02-components-and-types.md
|
|
17
|
-
│ ├── 03-data-and-state.md
|
|
18
|
-
│ ├── 04-design-system.md
|
|
19
|
-
│ ├── 05-security.md
|
|
20
|
-
│ └── 06-ux-checklist.md
|
|
21
|
-
└── server/ # Fastify backend rules
|
|
22
|
-
├── core.md # Index — which file to read for what
|
|
23
|
-
├── project-conventions.md
|
|
24
|
-
├── response-handling.md
|
|
25
|
-
└── database.md
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## For Claude
|
|
29
|
-
|
|
30
|
-
- This directory is the source of truth for project rules.
|
|
31
|
-
- **Don't read every file upfront.** Start with the relevant `core.md` index and follow it to the file you need.
|
|
32
|
-
- `global/core.md` always applies. `client/` and `server/` rules apply based on which directory you're working in.
|
|
33
|
-
|
|
34
|
-
## Key Principles (TL;DR)
|
|
35
|
-
|
|
36
|
-
### Global
|
|
37
|
-
- TypeScript strict, no `any`, Zod validation, explicit return types
|
|
38
|
-
- Small focused changes, extend don't rewrite, protect critical flows
|
|
39
|
-
- Conventional commits, `.env` never committed, tests for non-trivial changes
|
|
40
|
-
|
|
41
|
-
### Server
|
|
42
|
-
- **Stack**: Node.js, Fastify, TypeScript, MySQL, Prisma, Redis
|
|
43
|
-
- **Architecture**: Routes → Controllers → Services → Repositories → DB
|
|
44
|
-
- **API**: All routes prefixed with `/api/v1`
|
|
45
|
-
- **Responses**: `successResponse()` / `paginatedResponse()` — no custom shapes
|
|
46
|
-
- **Errors**: Only `AppError` subclasses, global error handler formats everything
|
|
47
|
-
|
|
48
|
-
### Client
|
|
49
|
-
- **Stack**: Next.js App Router, React Query, Redux (auth only), shadcn/ui, Tailwind
|
|
50
|
-
- **Architecture**: Server Components by default, `'use client'` only when needed
|
|
51
|
-
- **State**: Server data → Server Components or React Query. Redux → auth only. URL → filters/pagination
|
|
52
|
-
- **Design**: Semantic color tokens only, no hardcoded colors, neuro-minimalist aesthetic
|
|
53
|
-
- **Components**: Max 250 lines, max 5 props, max 3 JSX nesting levels
|