create-tigra 1.1.0 → 2.0.1
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/LICENSE +21 -21
- package/README.md +80 -87
- package/bin/create-tigra.js +259 -308
- package/package.json +49 -41
- package/template/_claude/QUICK_REFERENCE.md +193 -0
- package/template/_claude/README.md +53 -0
- package/template/_claude/commands/create-client.md +881 -0
- package/template/_claude/commands/create-server.md +383 -0
- package/template/_claude/rules/client/01-project-structure.md +133 -0
- package/template/_claude/rules/client/02-components-and-types.md +146 -0
- package/template/_claude/rules/client/03-data-and-state.md +156 -0
- package/template/_claude/rules/client/04-design-system.md +185 -0
- package/template/_claude/rules/client/05-security.md +55 -0
- package/template/_claude/rules/client/06-ux-checklist.md +81 -0
- package/template/_claude/rules/client/core.md +42 -0
- package/template/_claude/rules/global/core.md +77 -0
- package/template/_claude/rules/server/core.md +50 -0
- package/template/_claude/rules/server/database.md +124 -0
- package/template/_claude/rules/server/project-conventions.md +150 -0
- package/template/_claude/rules/server/response-handling.md +144 -0
- package/template/client/.env.example +5 -0
- package/template/client/README.md +36 -0
- package/template/client/components.json +23 -0
- package/template/client/eslint.config.mjs +18 -0
- package/template/client/next.config.ts +34 -0
- package/template/client/package.json +44 -0
- package/template/client/postcss.config.mjs +7 -0
- package/template/client/src/app/(auth)/layout.tsx +18 -0
- package/template/client/src/app/(auth)/login/page.tsx +13 -0
- package/template/client/src/app/(auth)/register/page.tsx +13 -0
- package/template/client/src/app/(main)/dashboard/page.tsx +22 -0
- package/template/client/src/app/(main)/layout.tsx +11 -0
- package/template/client/src/app/error.tsx +27 -0
- package/template/client/src/app/favicon.ico +0 -0
- package/template/client/src/app/globals.css +145 -0
- package/template/client/src/app/layout.tsx +36 -0
- package/template/client/src/app/loading.tsx +11 -0
- package/template/client/src/app/not-found.tsx +23 -0
- package/template/client/src/app/page.tsx +45 -0
- package/template/client/src/app/providers.tsx +43 -0
- package/template/client/src/components/common/ConfirmDialog.tsx +56 -0
- package/template/client/src/components/common/EmptyState.tsx +31 -0
- package/template/client/src/components/common/LoadingSpinner.tsx +30 -0
- package/template/client/src/components/common/Pagination.tsx +55 -0
- package/template/client/src/components/layout/Footer.tsx +17 -0
- package/template/client/src/components/layout/Header.tsx +173 -0
- package/template/client/src/components/layout/MainLayout.tsx +18 -0
- package/template/client/src/components/ui/alert-dialog.tsx +196 -0
- package/template/client/src/components/ui/badge.tsx +48 -0
- package/template/client/src/components/ui/button.tsx +64 -0
- package/template/client/src/components/ui/card.tsx +92 -0
- package/template/client/src/components/ui/input.tsx +21 -0
- package/template/client/src/components/ui/label.tsx +24 -0
- package/template/client/src/components/ui/select.tsx +190 -0
- package/template/client/src/components/ui/skeleton.tsx +13 -0
- package/template/client/src/components/ui/table.tsx +116 -0
- package/template/client/src/features/auth/components/AuthInitializer.tsx +55 -0
- package/template/client/src/features/auth/components/LoginForm.tsx +107 -0
- package/template/client/src/features/auth/components/RegisterForm.tsx +178 -0
- package/template/client/src/features/auth/hooks/useAuth.ts +84 -0
- package/template/client/src/features/auth/services/auth.service.ts +52 -0
- package/template/client/src/features/auth/store/authSlice.ts +38 -0
- package/template/client/src/features/auth/types/auth.types.ts +32 -0
- package/template/client/src/hooks/useDebounce.ts +14 -0
- package/template/client/src/hooks/useLocalStorage.ts +55 -0
- package/template/client/src/hooks/useMediaQuery.ts +27 -0
- package/template/client/src/lib/api/api.types.ts +34 -0
- package/template/client/src/lib/api/axios.config.ts +98 -0
- package/template/client/src/lib/constants/api-endpoints.ts +18 -0
- package/template/client/src/lib/constants/app.constants.ts +12 -0
- package/template/client/src/lib/constants/routes.ts +9 -0
- package/template/client/src/lib/utils/error.ts +32 -0
- package/template/client/src/lib/utils/format.ts +37 -0
- package/template/client/src/lib/utils/security.ts +34 -0
- package/template/client/src/lib/utils.ts +6 -0
- package/template/client/src/middleware.ts +57 -0
- package/template/client/src/store/hooks.ts +7 -0
- package/template/client/src/store/index.ts +12 -0
- package/template/client/src/types/index.ts +3 -0
- package/template/client/tsconfig.json +34 -0
- package/template/gitignore +34 -0
- package/template/server/.dockerignore +66 -0
- package/template/server/.env.example +96 -69
- package/template/server/.env.production.example +90 -0
- package/template/server/Dockerfile +94 -0
- package/template/server/docker-compose.yml +82 -111
- package/template/server/docs/logging.md +62 -0
- package/template/server/eslint.config.mjs +17 -0
- package/template/server/package.json +68 -81
- package/template/server/phpmyadmin-config.php +26 -0
- package/template/server/postman_collection.json +666 -0
- package/template/server/prisma/schema.prisma +77 -93
- package/template/server/prisma/seed.ts +46 -142
- package/template/server/scripts/flush-redis.ts +41 -0
- package/template/server/src/app.ts +243 -71
- package/template/server/src/config/env.ts +67 -94
- package/template/server/src/libs/auth.ts +88 -0
- package/template/server/src/libs/cleanup.ts +35 -0
- package/template/server/src/libs/cookies.ts +46 -0
- package/template/server/src/libs/logger.ts +33 -60
- package/template/server/src/libs/monitoring.ts +205 -0
- package/template/server/src/libs/password.ts +38 -0
- package/template/server/src/libs/prisma.ts +68 -0
- package/template/server/src/libs/redis.ts +60 -79
- package/template/server/src/libs/requestLogger.ts +66 -0
- package/template/server/src/libs/storage/file-storage.service.ts +211 -0
- package/template/server/src/libs/storage/file-validator.ts +97 -0
- package/template/server/src/libs/storage/filename-sanitizer.ts +71 -0
- package/template/server/src/libs/storage/image-optimizer.service.ts +144 -0
- package/template/server/src/modules/auth/__tests__/auth.service.test.ts +365 -0
- package/template/server/src/modules/auth/auth.controller.ts +90 -141
- package/template/server/src/modules/auth/auth.repo.ts +120 -218
- package/template/server/src/modules/auth/auth.routes.ts +96 -83
- package/template/server/src/modules/auth/auth.schemas.ts +35 -137
- package/template/server/src/modules/auth/auth.service.ts +286 -329
- package/template/server/src/modules/auth/session.repo.ts +110 -0
- package/template/server/src/modules/users/users.controller.ts +120 -0
- package/template/server/src/modules/users/users.repo.ts +77 -0
- package/template/server/src/modules/users/users.routes.ts +89 -0
- package/template/server/src/modules/users/users.schemas.ts +21 -0
- package/template/server/src/modules/users/users.service.ts +169 -0
- package/template/server/src/server.ts +58 -139
- package/template/server/src/shared/errors/AppError.ts +21 -0
- package/template/server/src/shared/errors/errors.ts +43 -0
- package/template/server/src/shared/responses/paginatedResponse.ts +38 -0
- package/template/server/src/shared/responses/successResponse.ts +17 -0
- package/template/server/src/shared/schemas/pagination.schema.ts +12 -0
- package/template/server/src/shared/types/index.ts +26 -0
- package/template/server/src/test/setup.ts +74 -38
- package/template/server/tsconfig.json +27 -89
- package/template/server/uploads/avatars/.gitkeep +1 -0
- package/template/server/vitest.config.ts +43 -98
- package/template/.agent/rules/client/01-project-structure.md +0 -326
- package/template/.agent/rules/client/02-component-patterns.md +0 -249
- package/template/.agent/rules/client/03-typescript-rules.md +0 -226
- package/template/.agent/rules/client/04-state-management.md +0 -474
- package/template/.agent/rules/client/05-api-integration.md +0 -129
- package/template/.agent/rules/client/06-forms-validation.md +0 -129
- package/template/.agent/rules/client/07-common-patterns.md +0 -150
- package/template/.agent/rules/client/08-color-system.md +0 -93
- package/template/.agent/rules/client/09-security-rules.md +0 -97
- package/template/.agent/rules/client/10-testing-strategy.md +0 -370
- package/template/.agent/rules/global/ai-edit-safety.md +0 -38
- package/template/.agent/rules/server/01-db-and-migrations.md +0 -242
- package/template/.agent/rules/server/02-general-rules.md +0 -111
- package/template/.agent/rules/server/03-migrations.md +0 -20
- package/template/.agent/rules/server/04-pagination.md +0 -130
- package/template/.agent/rules/server/05-project-conventions.md +0 -71
- package/template/.agent/rules/server/06-response-handling.md +0 -173
- package/template/.agent/rules/server/07-testing-strategy.md +0 -506
- package/template/.agent/rules/server/08-observability.md +0 -180
- package/template/.agent/rules/server/10-background-jobs-v2.md +0 -185
- package/template/.agent/rules/server/11-rate-limiting-v2.md +0 -210
- package/template/.agent/rules/server/12-performance-optimization.md +0 -567
- package/template/.claude/rules/client-01-project-structure.md +0 -327
- package/template/.claude/rules/client-02-component-patterns.md +0 -250
- package/template/.claude/rules/client-03-typescript-rules.md +0 -227
- package/template/.claude/rules/client-04-state-management.md +0 -475
- package/template/.claude/rules/client-05-api-integration.md +0 -130
- package/template/.claude/rules/client-06-forms-validation.md +0 -130
- package/template/.claude/rules/client-07-common-patterns.md +0 -151
- package/template/.claude/rules/client-08-color-system.md +0 -94
- package/template/.claude/rules/client-09-security-rules.md +0 -98
- package/template/.claude/rules/client-10-testing-strategy.md +0 -371
- package/template/.claude/rules/global-ai-edit-safety.md +0 -39
- package/template/.claude/rules/server-01-db-and-migrations.md +0 -243
- package/template/.claude/rules/server-02-general-rules.md +0 -112
- package/template/.claude/rules/server-03-migrations.md +0 -21
- package/template/.claude/rules/server-04-pagination.md +0 -131
- package/template/.claude/rules/server-05-project-conventions.md +0 -72
- package/template/.claude/rules/server-06-response-handling.md +0 -174
- package/template/.claude/rules/server-07-testing-strategy.md +0 -507
- package/template/.claude/rules/server-08-observability.md +0 -181
- package/template/.claude/rules/server-10-background-jobs-v2.md +0 -186
- package/template/.claude/rules/server-11-rate-limiting-v2.md +0 -211
- package/template/.claude/rules/server-12-performance-optimization.md +0 -568
- package/template/.cursor/rules/client-01-project-structure.mdc +0 -327
- package/template/.cursor/rules/client-02-component-patterns.mdc +0 -250
- package/template/.cursor/rules/client-03-typescript-rules.mdc +0 -227
- package/template/.cursor/rules/client-04-state-management.mdc +0 -475
- package/template/.cursor/rules/client-05-api-integration.mdc +0 -130
- package/template/.cursor/rules/client-06-forms-validation.mdc +0 -130
- package/template/.cursor/rules/client-07-common-patterns.mdc +0 -151
- package/template/.cursor/rules/client-08-color-system.mdc +0 -94
- package/template/.cursor/rules/client-09-security-rules.mdc +0 -98
- package/template/.cursor/rules/client-10-testing-strategy.mdc +0 -371
- package/template/.cursor/rules/global-ai-edit-safety.mdc +0 -39
- package/template/.cursor/rules/server-01-db-and-migrations.mdc +0 -243
- package/template/.cursor/rules/server-02-general-rules.mdc +0 -112
- package/template/.cursor/rules/server-03-migrations.mdc +0 -21
- package/template/.cursor/rules/server-04-pagination.mdc +0 -131
- package/template/.cursor/rules/server-05-project-conventions.mdc +0 -72
- package/template/.cursor/rules/server-06-response-handling.mdc +0 -174
- package/template/.cursor/rules/server-07-testing-strategy.mdc +0 -507
- package/template/.cursor/rules/server-08-observability.mdc +0 -181
- package/template/.cursor/rules/server-09-api-documentation-v2.mdc +0 -169
- package/template/.cursor/rules/server-10-background-jobs-v2.mdc +0 -186
- package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +0 -211
- package/template/.cursor/rules/server-12-performance-optimization.mdc +0 -568
- package/template/CLAUDE.md +0 -207
- package/template/server/.tsc-aliasrc.json +0 -13
- package/template/server/IMPORT_FIX_CHECKLIST.md +0 -98
- package/template/server/IMPORT_FIX_COMPLETE.md +0 -89
- package/template/server/README.md +0 -183
- package/template/server/REMAINING_IMPORT_FIXES.md +0 -150
- package/template/server/SECURITY.md +0 -190
- package/template/server/Tigra-API.postman_collection.json +0 -733
- package/template/server/biome.json +0 -42
- package/template/server/scripts/fix-all-imports.ps1 +0 -52
- package/template/server/scripts/fix-imports-reference.ps1 +0 -16
- package/template/server/scripts/fix-imports.mjs +0 -55
- package/template/server/scripts/setup-env.js +0 -50
- package/template/server/scripts/wait-for-db.js +0 -60
- package/template/server/src/hooks/request-timing.hook.ts +0 -26
- package/template/server/src/libs/auth/authenticate.middleware.ts +0 -22
- package/template/server/src/libs/auth/rbac.middleware.test.ts +0 -134
- package/template/server/src/libs/auth/rbac.middleware.ts +0 -147
- package/template/server/src/libs/db.ts +0 -76
- package/template/server/src/libs/error-handler.ts +0 -89
- package/template/server/src/libs/queue.ts +0 -79
- package/template/server/src/modules/admin/admin.controller.ts +0 -122
- package/template/server/src/modules/admin/admin.routes.ts +0 -62
- package/template/server/src/modules/admin/admin.schemas.ts +0 -35
- package/template/server/src/modules/admin/admin.service.ts +0 -167
- package/template/server/src/modules/auth/auth.integration.test.ts +0 -150
- package/template/server/src/modules/auth/auth.service.test.ts +0 -119
- package/template/server/src/modules/auth/auth.types.ts +0 -97
- package/template/server/src/modules/resources/resources.controller.ts +0 -218
- package/template/server/src/modules/resources/resources.repo.ts +0 -253
- package/template/server/src/modules/resources/resources.routes.ts +0 -116
- package/template/server/src/modules/resources/resources.schemas.ts +0 -146
- package/template/server/src/modules/resources/resources.service.ts +0 -218
- package/template/server/src/modules/resources/resources.types.ts +0 -73
- package/template/server/src/plugins/rate-limit.plugin.ts +0 -21
- package/template/server/src/plugins/security.plugin.ts +0 -21
- package/template/server/src/routes/health.routes.ts +0 -31
- package/template/server/src/types/fastify.d.ts +0 -36
- package/template/server/src/utils/errors.ts +0 -108
- package/template/server/src/utils/pagination.ts +0 -120
- package/template/server/src/utils/response.ts +0 -110
- package/template/server/src/workers/file.worker.ts +0 -106
- package/template/server/tsconfig.build.json +0 -30
- package/template/server/tsconfig.test.json +0 -22
|
@@ -1,329 +1,286 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
refreshToken
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Logout user
|
|
289
|
-
*
|
|
290
|
-
* Deletes the session associated with the refresh token.
|
|
291
|
-
*
|
|
292
|
-
* @param refreshToken - Refresh token
|
|
293
|
-
*/
|
|
294
|
-
export async function logout(refreshToken: string): Promise<void> {
|
|
295
|
-
try {
|
|
296
|
-
await authRepo.deleteSession(refreshToken);
|
|
297
|
-
logger.info('User logged out');
|
|
298
|
-
} catch (error) {
|
|
299
|
-
// Ignore errors if session doesn't exist
|
|
300
|
-
logger.warn({ error }, 'Logout failed - session may not exist');
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Verify access token
|
|
306
|
-
*
|
|
307
|
-
* @param token - Access token
|
|
308
|
-
* @returns JWT payload
|
|
309
|
-
* @throws UnauthorizedError if token is invalid
|
|
310
|
-
*/
|
|
311
|
-
export async function verifyAccessToken(token: string): Promise<JwtPayload> {
|
|
312
|
-
try {
|
|
313
|
-
const payload = jwt.verify(token, env.JWT_SECRET, {
|
|
314
|
-
issuer: env.JWT_ISSUER,
|
|
315
|
-
}) as JwtPayload;
|
|
316
|
-
|
|
317
|
-
return payload;
|
|
318
|
-
} catch (error) {
|
|
319
|
-
if (error instanceof jwt.TokenExpiredError) {
|
|
320
|
-
throw new UnauthorizedError('Access token expired');
|
|
321
|
-
}
|
|
322
|
-
if (error instanceof jwt.JsonWebTokenError) {
|
|
323
|
-
throw new UnauthorizedError('Invalid access token');
|
|
324
|
-
}
|
|
325
|
-
throw new UnauthorizedError('Token verification failed');
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
1
|
+
import { signAccessToken, generateRefreshToken, getRefreshTokenExpiresAt } from '@libs/auth.js';
|
|
2
|
+
import { hashPassword, verifyPassword } from '@libs/password.js';
|
|
3
|
+
import {
|
|
4
|
+
ConflictError,
|
|
5
|
+
UnauthorizedError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
} from '@shared/errors/errors.js';
|
|
8
|
+
import type { UserRole } from '@shared/types/index.js';
|
|
9
|
+
import * as authRepo from './auth.repo.js';
|
|
10
|
+
import { sessionRepository } from './session.repo.js';
|
|
11
|
+
import type { RegisterInput, LoginInput } from './auth.schemas.js';
|
|
12
|
+
|
|
13
|
+
// Account lockout configuration
|
|
14
|
+
const LOCKOUT_THRESHOLDS = [
|
|
15
|
+
{ attempts: 5, durationMs: 15 * 60 * 1000 }, // 5 failures → 15 min
|
|
16
|
+
{ attempts: 10, durationMs: 30 * 60 * 1000 }, // 10 failures → 30 min
|
|
17
|
+
{ attempts: 15, durationMs: 60 * 60 * 1000 }, // 15+ failures → 1 hour
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function getLockoutDuration(failedAttempts: number): number | null {
|
|
21
|
+
for (let i = LOCKOUT_THRESHOLDS.length - 1; i >= 0; i--) {
|
|
22
|
+
if (failedAttempts >= LOCKOUT_THRESHOLDS[i].attempts) {
|
|
23
|
+
return LOCKOUT_THRESHOLDS[i].durationMs;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SanitizedUser {
|
|
30
|
+
id: string;
|
|
31
|
+
email: string;
|
|
32
|
+
firstName: string;
|
|
33
|
+
lastName: string;
|
|
34
|
+
avatarUrl: string | null;
|
|
35
|
+
role: UserRole;
|
|
36
|
+
isActive: boolean;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface AuthResult {
|
|
42
|
+
user: SanitizedUser;
|
|
43
|
+
accessToken: string;
|
|
44
|
+
refreshToken: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sanitizeUser(user: {
|
|
48
|
+
id: string;
|
|
49
|
+
email: string;
|
|
50
|
+
firstName: string;
|
|
51
|
+
lastName: string;
|
|
52
|
+
avatarUrl?: string | null;
|
|
53
|
+
role: UserRole;
|
|
54
|
+
isActive: boolean;
|
|
55
|
+
createdAt: Date;
|
|
56
|
+
updatedAt: Date;
|
|
57
|
+
password: string;
|
|
58
|
+
}): SanitizedUser {
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
60
|
+
const { password: _password, createdAt, updatedAt, avatarUrl, ...rest } = user;
|
|
61
|
+
return {
|
|
62
|
+
...rest,
|
|
63
|
+
avatarUrl: avatarUrl ?? null,
|
|
64
|
+
createdAt: createdAt.toISOString(),
|
|
65
|
+
updatedAt: updatedAt.toISOString(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function register(
|
|
70
|
+
input: RegisterInput,
|
|
71
|
+
deviceInfo?: string,
|
|
72
|
+
ipAddress?: string,
|
|
73
|
+
): Promise<AuthResult> {
|
|
74
|
+
const existingUser = await authRepo.findUserByEmail(input.email);
|
|
75
|
+
if (existingUser) {
|
|
76
|
+
throw new ConflictError('Email already registered', 'EMAIL_ALREADY_EXISTS');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hashedPassword = await hashPassword(input.password);
|
|
80
|
+
|
|
81
|
+
const user = await authRepo.createUser({
|
|
82
|
+
email: input.email,
|
|
83
|
+
password: hashedPassword,
|
|
84
|
+
firstName: input.firstName,
|
|
85
|
+
lastName: input.lastName,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const accessToken = signAccessToken({
|
|
89
|
+
userId: user.id,
|
|
90
|
+
role: user.role,
|
|
91
|
+
});
|
|
92
|
+
const refreshToken = generateRefreshToken();
|
|
93
|
+
const refreshTokenExpiresAt = getRefreshTokenExpiresAt();
|
|
94
|
+
|
|
95
|
+
await authRepo.createRefreshToken({
|
|
96
|
+
token: refreshToken,
|
|
97
|
+
userId: user.id,
|
|
98
|
+
expiresAt: refreshTokenExpiresAt,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await sessionRepository.createSession({
|
|
102
|
+
userId: user.id,
|
|
103
|
+
deviceInfo,
|
|
104
|
+
ipAddress,
|
|
105
|
+
expiresAt: refreshTokenExpiresAt,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
user: sanitizeUser(user),
|
|
110
|
+
accessToken,
|
|
111
|
+
refreshToken,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function login(
|
|
116
|
+
input: LoginInput,
|
|
117
|
+
deviceInfo?: string,
|
|
118
|
+
ipAddress?: string,
|
|
119
|
+
): Promise<AuthResult> {
|
|
120
|
+
const user = await authRepo.findUserByEmail(input.email);
|
|
121
|
+
if (!user) {
|
|
122
|
+
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Generic error for disabled accounts — prevent info leakage
|
|
126
|
+
if (!user.isActive) {
|
|
127
|
+
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check account lockout
|
|
131
|
+
if (user.lockedUntil && user.lockedUntil > new Date()) {
|
|
132
|
+
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { valid, needsRehash } = await verifyPassword(input.password, user.password);
|
|
136
|
+
|
|
137
|
+
if (!valid) {
|
|
138
|
+
// Increment failed attempts
|
|
139
|
+
const newAttempts = user.failedLoginAttempts + 1;
|
|
140
|
+
await authRepo.incrementFailedAttempts(user.id);
|
|
141
|
+
|
|
142
|
+
// Check if we need to lock the account
|
|
143
|
+
const lockDuration = getLockoutDuration(newAttempts);
|
|
144
|
+
if (lockDuration) {
|
|
145
|
+
await authRepo.setAccountLock(user.id, new Date(Date.now() + lockDuration));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Successful login — reset failed attempts
|
|
152
|
+
if (user.failedLoginAttempts > 0 || user.lockedUntil) {
|
|
153
|
+
await authRepo.resetFailedAttempts(user.id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Transparent rehash: upgrade bcrypt → argon2id
|
|
157
|
+
if (needsRehash) {
|
|
158
|
+
const newHash = await hashPassword(input.password);
|
|
159
|
+
await authRepo.updateUserPassword(user.id, newHash);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const accessToken = signAccessToken({
|
|
163
|
+
userId: user.id,
|
|
164
|
+
role: user.role,
|
|
165
|
+
});
|
|
166
|
+
const refreshToken = generateRefreshToken();
|
|
167
|
+
const refreshTokenExpiresAt = getRefreshTokenExpiresAt();
|
|
168
|
+
|
|
169
|
+
await authRepo.createRefreshToken({
|
|
170
|
+
token: refreshToken,
|
|
171
|
+
userId: user.id,
|
|
172
|
+
expiresAt: refreshTokenExpiresAt,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await sessionRepository.createSession({
|
|
176
|
+
userId: user.id,
|
|
177
|
+
deviceInfo,
|
|
178
|
+
ipAddress,
|
|
179
|
+
expiresAt: refreshTokenExpiresAt,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
user: sanitizeUser(user),
|
|
184
|
+
accessToken,
|
|
185
|
+
refreshToken,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function refresh(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
|
|
190
|
+
const storedToken = await authRepo.findRefreshToken(refreshToken);
|
|
191
|
+
if (!storedToken) {
|
|
192
|
+
throw new UnauthorizedError('Invalid refresh token', 'INVALID_REFRESH_TOKEN');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (new Date() > storedToken.expiresAt) {
|
|
196
|
+
await authRepo.deleteRefreshToken(refreshToken);
|
|
197
|
+
throw new UnauthorizedError('Refresh token expired', 'REFRESH_TOKEN_EXPIRED');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const user = await authRepo.findUserById(storedToken.userId);
|
|
201
|
+
if (!user || !user.isActive) {
|
|
202
|
+
throw new UnauthorizedError('User not found or disabled', 'INVALID_REFRESH_TOKEN');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const newAccessToken = signAccessToken({
|
|
206
|
+
userId: user.id,
|
|
207
|
+
role: user.role,
|
|
208
|
+
});
|
|
209
|
+
const newRefreshToken = generateRefreshToken();
|
|
210
|
+
|
|
211
|
+
// Atomic rotation: delete old + create new in a single transaction.
|
|
212
|
+
// Returns false if old token was already consumed by a concurrent request.
|
|
213
|
+
const rotated = await authRepo.rotateRefreshToken(refreshToken, {
|
|
214
|
+
token: newRefreshToken,
|
|
215
|
+
userId: user.id,
|
|
216
|
+
expiresAt: getRefreshTokenExpiresAt(),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!rotated) {
|
|
220
|
+
throw new UnauthorizedError('Refresh token already used', 'INVALID_REFRESH_TOKEN');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
accessToken: newAccessToken,
|
|
225
|
+
refreshToken: newRefreshToken,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function logout(refreshToken: string): Promise<void> {
|
|
230
|
+
try {
|
|
231
|
+
// Find the token before deleting so we can match the corresponding session
|
|
232
|
+
const storedToken = await authRepo.findRefreshToken(refreshToken);
|
|
233
|
+
await authRepo.deleteRefreshToken(refreshToken);
|
|
234
|
+
|
|
235
|
+
if (storedToken) {
|
|
236
|
+
// Match session by creation time — token and session are created together during login
|
|
237
|
+
const sessions = await sessionRepository.getUserSessions(storedToken.userId);
|
|
238
|
+
const tokenCreatedMs = storedToken.createdAt.getTime();
|
|
239
|
+
const matchingSession = sessions.find(
|
|
240
|
+
(s) => Math.abs(s.createdAt.getTime() - tokenCreatedMs) < 5000,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (matchingSession) {
|
|
244
|
+
await sessionRepository.deleteSession(matchingSession.id);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// Token may already be deleted by concurrent request or cleanup job
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function getCurrentUser(userId: string): Promise<SanitizedUser> {
|
|
253
|
+
const user = await authRepo.findUserById(userId);
|
|
254
|
+
if (!user) {
|
|
255
|
+
throw new NotFoundError('User not found', 'USER_NOT_FOUND');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return sanitizeUser(user);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface SessionInfo {
|
|
262
|
+
id: string;
|
|
263
|
+
deviceInfo: string | null;
|
|
264
|
+
ipAddress: string | null;
|
|
265
|
+
lastActiveAt: string;
|
|
266
|
+
expiresAt: string;
|
|
267
|
+
createdAt: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function getUserSessions(userId: string): Promise<SessionInfo[]> {
|
|
271
|
+
const sessions = await sessionRepository.getUserSessions(userId);
|
|
272
|
+
return sessions.map((session) => ({
|
|
273
|
+
id: session.id,
|
|
274
|
+
deviceInfo: session.deviceInfo,
|
|
275
|
+
ipAddress: session.ipAddress,
|
|
276
|
+
lastActiveAt: session.lastActiveAt.toISOString(),
|
|
277
|
+
expiresAt: session.expiresAt.toISOString(),
|
|
278
|
+
createdAt: session.createdAt.toISOString(),
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function logoutAllSessions(userId: string): Promise<number> {
|
|
283
|
+
const sessionCount = await sessionRepository.deleteAllUserSessions(userId);
|
|
284
|
+
await authRepo.deleteRefreshTokensByUserId(userId);
|
|
285
|
+
return sessionCount;
|
|
286
|
+
}
|