create-fluxstack 1.0.13 → 1.0.15
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/.env.example +29 -29
- package/app/client/README.md +69 -69
- package/app/client/index.html +14 -13
- package/app/client/src/App.tsx +157 -524
- package/app/client/src/components/ErrorBoundary.tsx +107 -0
- package/app/client/src/components/ErrorDisplay.css +365 -0
- package/app/client/src/components/ErrorDisplay.tsx +258 -0
- package/app/client/src/components/FluxStackConfig.tsx +1321 -0
- package/app/client/src/components/HybridLiveCounter.tsx +140 -0
- package/app/client/src/components/LiveClock.tsx +286 -0
- package/app/client/src/components/MainLayout.tsx +390 -0
- package/app/client/src/components/SidebarNavigation.tsx +391 -0
- package/app/client/src/components/StateDemo.tsx +178 -0
- package/app/client/src/components/SystemMonitor.tsx +1038 -0
- package/app/client/src/components/Teste.tsx +104 -0
- package/app/client/src/components/UserProfile.tsx +809 -0
- package/app/client/src/hooks/useAuth.ts +39 -0
- package/app/client/src/hooks/useNotifications.ts +56 -0
- package/app/client/src/lib/eden-api.ts +189 -53
- package/app/client/src/lib/errors.ts +340 -0
- package/app/client/src/lib/hooks/useErrorHandler.ts +258 -0
- package/app/client/src/lib/index.ts +45 -0
- package/app/client/src/main.tsx +3 -2
- package/app/client/src/pages/ApiDocs.tsx +182 -0
- package/app/client/src/pages/Demo.tsx +174 -0
- package/app/client/src/pages/HybridLive.tsx +263 -0
- package/app/client/src/pages/Overview.tsx +155 -0
- package/app/client/src/store/README.md +43 -0
- package/app/client/src/store/index.ts +16 -0
- package/app/client/src/store/slices/uiSlice.ts +151 -0
- package/app/client/src/store/slices/userSlice.ts +161 -0
- package/app/client/src/test/README.md +257 -0
- package/app/client/src/test/setup.ts +70 -0
- package/app/client/src/test/types.ts +12 -0
- package/app/client/src/vite-env.d.ts +1 -1
- package/app/client/tsconfig.app.json +44 -43
- package/app/client/tsconfig.json +7 -7
- package/app/client/tsconfig.node.json +25 -25
- package/app/client/zustand-setup.md +65 -0
- package/app/server/controllers/users.controller.ts +68 -68
- package/app/server/index.ts +9 -1
- package/app/server/live/CounterComponent.ts +191 -0
- package/app/server/live/FluxStackConfig.ts +529 -0
- package/app/server/live/LiveClockComponent.ts +214 -0
- package/app/server/live/SidebarNavigation.ts +156 -0
- package/app/server/live/SystemMonitor.ts +594 -0
- package/app/server/live/SystemMonitorIntegration.ts +151 -0
- package/app/server/live/TesteComponent.ts +87 -0
- package/app/server/live/UserProfileComponent.ts +135 -0
- package/app/server/live/register-components.ts +28 -0
- package/app/server/middleware/auth.ts +136 -0
- package/app/server/middleware/errorHandling.ts +250 -0
- package/app/server/middleware/index.ts +10 -0
- package/app/server/middleware/rateLimit.ts +193 -0
- package/app/server/middleware/requestLogging.ts +215 -0
- package/app/server/middleware/validation.ts +270 -0
- package/app/server/routes/index.ts +14 -2
- package/app/server/routes/upload.ts +92 -0
- package/app/server/routes/users.routes.ts +2 -9
- package/app/server/services/NotificationService.ts +302 -0
- package/app/server/services/UserService.ts +222 -0
- package/app/server/services/index.ts +46 -0
- package/core/cli/commands/plugin-deps.ts +263 -0
- package/core/cli/generators/README.md +339 -0
- package/core/cli/generators/component.ts +770 -0
- package/core/cli/generators/controller.ts +299 -0
- package/core/cli/generators/index.ts +144 -0
- package/core/cli/generators/interactive.ts +228 -0
- package/core/cli/generators/prompts.ts +83 -0
- package/core/cli/generators/route.ts +513 -0
- package/core/cli/generators/service.ts +465 -0
- package/core/cli/generators/template-engine.ts +154 -0
- package/core/cli/generators/types.ts +71 -0
- package/core/cli/generators/utils.ts +192 -0
- package/core/cli/index.ts +69 -0
- package/core/cli/plugin-discovery.ts +16 -85
- package/core/client/fluxstack.ts +17 -0
- package/core/client/hooks/index.ts +7 -0
- package/core/client/hooks/state-validator.ts +130 -0
- package/core/client/hooks/useAuth.ts +49 -0
- package/core/client/hooks/useChunkedUpload.ts +258 -0
- package/core/client/hooks/useHybridLiveComponent.ts +967 -0
- package/core/client/hooks/useWebSocket.ts +373 -0
- package/core/client/index.ts +47 -0
- package/core/client/state/createStore.ts +193 -0
- package/core/client/state/index.ts +15 -0
- package/core/config/env-dynamic.ts +1 -1
- package/core/config/env.ts +2 -1
- package/core/config/runtime-config.ts +3 -3
- package/core/config/schema.ts +84 -49
- package/core/framework/server.ts +30 -0
- package/core/index.ts +25 -0
- package/core/live/ComponentRegistry.ts +399 -0
- package/core/live/types.ts +164 -0
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +1201 -0
- package/core/plugins/built-in/live-components/index.ts +27 -0
- package/core/plugins/built-in/logger/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +1 -1
- package/core/plugins/built-in/static/index.ts +1 -1
- package/core/plugins/built-in/swagger/index.ts +1 -1
- package/core/plugins/built-in/vite/index.ts +1 -1
- package/core/plugins/dependency-manager.ts +384 -0
- package/core/plugins/index.ts +5 -1
- package/core/plugins/manager.ts +7 -3
- package/core/plugins/registry.ts +88 -10
- package/core/plugins/types.ts +11 -11
- package/core/server/framework.ts +43 -0
- package/core/server/index.ts +11 -1
- package/core/server/live/ComponentRegistry.ts +1017 -0
- package/core/server/live/FileUploadManager.ts +272 -0
- package/core/server/live/LiveComponentPerformanceMonitor.ts +930 -0
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +644 -0
- package/core/server/live/WebSocketConnectionManager.ts +688 -0
- package/core/server/live/websocket-plugin.ts +435 -0
- package/core/server/middleware/errorHandling.ts +141 -0
- package/core/server/middleware/index.ts +16 -0
- package/core/server/plugins/static-files-plugin.ts +232 -0
- package/core/server/services/BaseService.ts +95 -0
- package/core/server/services/ServiceContainer.ts +144 -0
- package/core/server/services/index.ts +9 -0
- package/core/templates/create-project.ts +196 -33
- package/core/testing/index.ts +10 -0
- package/core/testing/setup.ts +74 -0
- package/core/types/build.ts +38 -14
- package/core/types/types.ts +319 -0
- package/core/utils/env-runtime.ts +7 -0
- package/core/utils/errors/handlers.ts +264 -39
- package/core/utils/errors/index.ts +528 -18
- package/core/utils/errors/middleware.ts +114 -0
- package/core/utils/logger/formatters.ts +222 -0
- package/core/utils/logger/index.ts +167 -48
- package/core/utils/logger/middleware.ts +253 -0
- package/core/utils/logger/performance.ts +384 -0
- package/core/utils/logger/transports.ts +365 -0
- package/create-fluxstack.ts +296 -296
- package/fluxstack.config.ts +17 -1
- package/package-template.json +66 -66
- package/package.json +31 -6
- package/public/README.md +16 -0
- package/vite.config.ts +29 -14
- package/.claude/settings.local.json +0 -74
- package/.github/workflows/ci-build-tests.yml +0 -480
- package/.github/workflows/dependency-management.yml +0 -324
- package/.github/workflows/release-validation.yml +0 -355
- package/.kiro/specs/fluxstack-architecture-optimization/design.md +0 -700
- package/.kiro/specs/fluxstack-architecture-optimization/requirements.md +0 -127
- package/.kiro/specs/fluxstack-architecture-optimization/tasks.md +0 -330
- package/CLAUDE.md +0 -200
- package/Dockerfile +0 -58
- package/Dockerfile.backend +0 -52
- package/Dockerfile.frontend +0 -54
- package/README-Docker.md +0 -85
- package/ai-context/00-QUICK-START.md +0 -86
- package/ai-context/README.md +0 -88
- package/ai-context/development/eden-treaty-guide.md +0 -362
- package/ai-context/development/patterns.md +0 -382
- package/ai-context/development/plugins-guide.md +0 -572
- package/ai-context/examples/crud-complete.md +0 -626
- package/ai-context/project/architecture.md +0 -399
- package/ai-context/project/overview.md +0 -213
- package/ai-context/recent-changes/eden-treaty-refactor.md +0 -281
- package/ai-context/recent-changes/type-inference-fix.md +0 -223
- package/ai-context/reference/environment-vars.md +0 -384
- package/ai-context/reference/troubleshooting.md +0 -407
- package/app/client/src/components/TestPage.tsx +0 -453
- package/bun.lock +0 -1063
- package/bunfig.toml +0 -16
- package/core/__tests__/integration.test.ts +0 -227
- package/core/build/index.ts +0 -186
- package/core/config/__tests__/config-loader.test.ts +0 -554
- package/core/config/__tests__/config-merger.test.ts +0 -657
- package/core/config/__tests__/env-converter.test.ts +0 -372
- package/core/config/__tests__/env-processor.test.ts +0 -431
- package/core/config/__tests__/env.test.ts +0 -452
- package/core/config/__tests__/integration.test.ts +0 -418
- package/core/config/__tests__/loader.test.ts +0 -331
- package/core/config/__tests__/schema.test.ts +0 -129
- package/core/config/__tests__/validator.test.ts +0 -318
- package/core/framework/__tests__/server.test.ts +0 -233
- package/core/plugins/__tests__/built-in.test.ts.disabled +0 -366
- package/core/plugins/__tests__/manager.test.ts +0 -398
- package/core/plugins/__tests__/monitoring.test.ts +0 -401
- package/core/plugins/__tests__/registry.test.ts +0 -335
- package/core/utils/__tests__/errors.test.ts +0 -139
- package/core/utils/__tests__/helpers.test.ts +0 -297
- package/core/utils/__tests__/logger.test.ts +0 -141
- package/create-test-app.ts +0 -156
- package/docker-compose.microservices.yml +0 -75
- package/docker-compose.simple.yml +0 -57
- package/docker-compose.yml +0 -71
- package/eslint.config.js +0 -23
- package/flux-cli.ts +0 -214
- package/nginx-lb.conf +0 -37
- package/publish.sh +0 -63
- package/run-clean.ts +0 -26
- package/run-env-tests.ts +0 -313
- package/tailwind.config.js +0 -34
- package/tests/__mocks__/api.ts +0 -56
- package/tests/fixtures/users.ts +0 -69
- package/tests/integration/api/users.routes.test.ts +0 -221
- package/tests/setup.ts +0 -29
- package/tests/unit/app/client/App-simple.test.tsx +0 -56
- package/tests/unit/app/client/App.test.tsx.skip +0 -237
- package/tests/unit/app/client/eden-api.test.ts +0 -186
- package/tests/unit/app/client/simple.test.tsx +0 -23
- package/tests/unit/app/controllers/users.controller.test.ts +0 -150
- package/tests/unit/core/create-project.test.ts.skip +0 -95
- package/tests/unit/core/framework.test.ts +0 -144
- package/tests/unit/core/plugins/logger.test.ts.skip +0 -268
- package/tests/unit/core/plugins/vite.test.ts.disabled +0 -188
- package/tests/utils/test-helpers.ts +0 -61
- package/vitest.config.ts +0 -50
- package/workspace.json +0 -6
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handling Middleware
|
|
3
|
+
* Provides centralized error handling for the application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Context } from 'elysia'
|
|
7
|
+
|
|
8
|
+
export interface ErrorResponse {
|
|
9
|
+
error: string
|
|
10
|
+
message: string
|
|
11
|
+
code?: string
|
|
12
|
+
details?: any
|
|
13
|
+
timestamp: string
|
|
14
|
+
requestId?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Custom application errors
|
|
19
|
+
*/
|
|
20
|
+
export class AppError extends Error {
|
|
21
|
+
constructor(
|
|
22
|
+
message: string,
|
|
23
|
+
public statusCode: number = 500,
|
|
24
|
+
public code?: string,
|
|
25
|
+
public details?: any
|
|
26
|
+
) {
|
|
27
|
+
super(message)
|
|
28
|
+
this.name = 'AppError'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ValidationError extends AppError {
|
|
33
|
+
constructor(message: string, details?: any) {
|
|
34
|
+
super(message, 400, 'VALIDATION_ERROR', details)
|
|
35
|
+
this.name = 'ValidationError'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class NotFoundError extends AppError {
|
|
40
|
+
constructor(resource: string = 'Resource') {
|
|
41
|
+
super(`${resource} not found`, 404, 'NOT_FOUND')
|
|
42
|
+
this.name = 'NotFoundError'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class UnauthorizedError extends AppError {
|
|
47
|
+
constructor(message: string = 'Unauthorized') {
|
|
48
|
+
super(message, 401, 'UNAUTHORIZED')
|
|
49
|
+
this.name = 'UnauthorizedError'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ForbiddenError extends AppError {
|
|
54
|
+
constructor(message: string = 'Forbidden') {
|
|
55
|
+
super(message, 403, 'FORBIDDEN')
|
|
56
|
+
this.name = 'ForbiddenError'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ConflictError extends AppError {
|
|
61
|
+
constructor(message: string, details?: any) {
|
|
62
|
+
super(message, 409, 'CONFLICT', details)
|
|
63
|
+
this.name = 'ConflictError'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Error handling middleware
|
|
69
|
+
*/
|
|
70
|
+
export const errorHandlingMiddleware = {
|
|
71
|
+
name: 'error-handling',
|
|
72
|
+
|
|
73
|
+
onError: async (context: Context, error: Error): Promise<Response> => {
|
|
74
|
+
const requestId = context.store?.requestId || 'unknown'
|
|
75
|
+
|
|
76
|
+
// Log the error
|
|
77
|
+
console.error('🚨 Error occurred', {
|
|
78
|
+
requestId,
|
|
79
|
+
method: context.request.method,
|
|
80
|
+
path: context.path,
|
|
81
|
+
error: error.message,
|
|
82
|
+
stack: error.stack,
|
|
83
|
+
timestamp: new Date().toISOString()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Handle different error types
|
|
87
|
+
if (error instanceof AppError) {
|
|
88
|
+
return createErrorResponse(
|
|
89
|
+
error.statusCode,
|
|
90
|
+
error.message,
|
|
91
|
+
error.code,
|
|
92
|
+
error.details,
|
|
93
|
+
requestId
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle validation errors from Elysia
|
|
98
|
+
if (error.name === 'ValidationError' || error.message.includes('validation')) {
|
|
99
|
+
return createErrorResponse(
|
|
100
|
+
400,
|
|
101
|
+
'Validation failed',
|
|
102
|
+
'VALIDATION_ERROR',
|
|
103
|
+
{ originalError: error.message },
|
|
104
|
+
requestId
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle syntax errors (malformed JSON, etc.)
|
|
109
|
+
if (error instanceof SyntaxError) {
|
|
110
|
+
return createErrorResponse(
|
|
111
|
+
400,
|
|
112
|
+
'Invalid request format',
|
|
113
|
+
'SYNTAX_ERROR',
|
|
114
|
+
undefined,
|
|
115
|
+
requestId
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle network/timeout errors
|
|
120
|
+
if (error.name === 'TimeoutError' || error.message.includes('timeout')) {
|
|
121
|
+
return createErrorResponse(
|
|
122
|
+
408,
|
|
123
|
+
'Request timeout',
|
|
124
|
+
'TIMEOUT_ERROR',
|
|
125
|
+
undefined,
|
|
126
|
+
requestId
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Handle database/external service errors
|
|
131
|
+
if (error.message.includes('ECONNREFUSED') || error.message.includes('connection')) {
|
|
132
|
+
return createErrorResponse(
|
|
133
|
+
503,
|
|
134
|
+
'Service temporarily unavailable',
|
|
135
|
+
'SERVICE_UNAVAILABLE',
|
|
136
|
+
undefined,
|
|
137
|
+
requestId
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Default to internal server error
|
|
142
|
+
return createErrorResponse(
|
|
143
|
+
500,
|
|
144
|
+
process.env.NODE_ENV === 'production'
|
|
145
|
+
? 'Internal server error'
|
|
146
|
+
: error.message,
|
|
147
|
+
'INTERNAL_ERROR',
|
|
148
|
+
process.env.NODE_ENV === 'production'
|
|
149
|
+
? undefined
|
|
150
|
+
: { stack: error.stack },
|
|
151
|
+
requestId
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create standardized error response
|
|
158
|
+
*/
|
|
159
|
+
function createErrorResponse(
|
|
160
|
+
statusCode: number,
|
|
161
|
+
message: string,
|
|
162
|
+
code?: string,
|
|
163
|
+
details?: any,
|
|
164
|
+
requestId?: string
|
|
165
|
+
): Response {
|
|
166
|
+
const errorResponse: ErrorResponse = {
|
|
167
|
+
error: getErrorName(statusCode),
|
|
168
|
+
message,
|
|
169
|
+
code,
|
|
170
|
+
details,
|
|
171
|
+
timestamp: new Date().toISOString(),
|
|
172
|
+
requestId
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Remove undefined fields
|
|
176
|
+
Object.keys(errorResponse).forEach(key => {
|
|
177
|
+
if (errorResponse[key as keyof ErrorResponse] === undefined) {
|
|
178
|
+
delete errorResponse[key as keyof ErrorResponse]
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return new Response(
|
|
183
|
+
JSON.stringify(errorResponse),
|
|
184
|
+
{
|
|
185
|
+
status: statusCode,
|
|
186
|
+
headers: {
|
|
187
|
+
'Content-Type': 'application/json'
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get error name from status code
|
|
195
|
+
*/
|
|
196
|
+
function getErrorName(statusCode: number): string {
|
|
197
|
+
const errorNames: Record<number, string> = {
|
|
198
|
+
400: 'Bad Request',
|
|
199
|
+
401: 'Unauthorized',
|
|
200
|
+
403: 'Forbidden',
|
|
201
|
+
404: 'Not Found',
|
|
202
|
+
405: 'Method Not Allowed',
|
|
203
|
+
408: 'Request Timeout',
|
|
204
|
+
409: 'Conflict',
|
|
205
|
+
422: 'Unprocessable Entity',
|
|
206
|
+
429: 'Too Many Requests',
|
|
207
|
+
500: 'Internal Server Error',
|
|
208
|
+
502: 'Bad Gateway',
|
|
209
|
+
503: 'Service Unavailable',
|
|
210
|
+
504: 'Gateway Timeout'
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return errorNames[statusCode] || 'Unknown Error'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Async error wrapper for route handlers
|
|
218
|
+
*/
|
|
219
|
+
export const asyncHandler = (fn: Function) => {
|
|
220
|
+
return async (context: Context) => {
|
|
221
|
+
try {
|
|
222
|
+
return await fn(context)
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw error // Let the error middleware handle it
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create error response helper
|
|
231
|
+
*/
|
|
232
|
+
export const createError = {
|
|
233
|
+
badRequest: (message: string, details?: any) =>
|
|
234
|
+
new ValidationError(message, details),
|
|
235
|
+
|
|
236
|
+
unauthorized: (message?: string) =>
|
|
237
|
+
new UnauthorizedError(message),
|
|
238
|
+
|
|
239
|
+
forbidden: (message?: string) =>
|
|
240
|
+
new ForbiddenError(message),
|
|
241
|
+
|
|
242
|
+
notFound: (resource?: string) =>
|
|
243
|
+
new NotFoundError(resource),
|
|
244
|
+
|
|
245
|
+
conflict: (message: string, details?: any) =>
|
|
246
|
+
new ConflictError(message, details),
|
|
247
|
+
|
|
248
|
+
internal: (message: string, details?: any) =>
|
|
249
|
+
new AppError(message, 500, 'INTERNAL_ERROR', details)
|
|
250
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware Index
|
|
3
|
+
* Exports all custom middleware for the application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { authMiddleware } from './auth'
|
|
7
|
+
export { validationMiddleware } from './validation'
|
|
8
|
+
export { rateLimitMiddleware } from './rateLimit'
|
|
9
|
+
export { requestLoggingMiddleware } from './requestLogging'
|
|
10
|
+
export { errorHandlingMiddleware } from './errorHandling'
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiting Middleware
|
|
3
|
+
* Implements rate limiting to prevent abuse
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Context } from 'elysia'
|
|
7
|
+
|
|
8
|
+
export interface RateLimitConfig {
|
|
9
|
+
windowMs: number // Time window in milliseconds
|
|
10
|
+
maxRequests: number // Maximum requests per window
|
|
11
|
+
keyGenerator?: (context: Context) => string // Custom key generator
|
|
12
|
+
skipSuccessfulRequests?: boolean // Don't count successful requests
|
|
13
|
+
skipFailedRequests?: boolean // Don't count failed requests
|
|
14
|
+
message?: string // Custom error message
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RateLimitEntry {
|
|
18
|
+
count: number
|
|
19
|
+
resetTime: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* In-memory rate limit store
|
|
24
|
+
* In production, you'd want to use Redis or another distributed store
|
|
25
|
+
*/
|
|
26
|
+
class MemoryStore {
|
|
27
|
+
private store = new Map<string, RateLimitEntry>()
|
|
28
|
+
|
|
29
|
+
get(key: string): RateLimitEntry | undefined {
|
|
30
|
+
const entry = this.store.get(key)
|
|
31
|
+
|
|
32
|
+
// Clean up expired entries
|
|
33
|
+
if (entry && entry.resetTime < Date.now()) {
|
|
34
|
+
this.store.delete(key)
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return entry
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set(key: string, entry: RateLimitEntry): void {
|
|
42
|
+
this.store.set(key, entry)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
increment(key: string, windowMs: number): RateLimitEntry {
|
|
46
|
+
const now = Date.now()
|
|
47
|
+
const existing = this.get(key)
|
|
48
|
+
|
|
49
|
+
if (existing) {
|
|
50
|
+
existing.count++
|
|
51
|
+
return existing
|
|
52
|
+
} else {
|
|
53
|
+
const newEntry: RateLimitEntry = {
|
|
54
|
+
count: 1,
|
|
55
|
+
resetTime: now + windowMs
|
|
56
|
+
}
|
|
57
|
+
this.set(key, newEntry)
|
|
58
|
+
return newEntry
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
cleanup(): void {
|
|
63
|
+
const now = Date.now()
|
|
64
|
+
for (const [key, entry] of this.store.entries()) {
|
|
65
|
+
if (entry.resetTime < now) {
|
|
66
|
+
this.store.delete(key)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const store = new MemoryStore()
|
|
73
|
+
|
|
74
|
+
// Cleanup expired entries every minute
|
|
75
|
+
setInterval(() => {
|
|
76
|
+
store.cleanup()
|
|
77
|
+
}, 60000)
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create rate limiting middleware
|
|
81
|
+
*/
|
|
82
|
+
export const rateLimitMiddleware = (config: RateLimitConfig) => ({
|
|
83
|
+
name: 'rate-limit',
|
|
84
|
+
|
|
85
|
+
beforeHandle: async (context: Context) => {
|
|
86
|
+
const key = config.keyGenerator
|
|
87
|
+
? config.keyGenerator(context)
|
|
88
|
+
: getDefaultKey(context)
|
|
89
|
+
|
|
90
|
+
const entry = store.increment(key, config.windowMs)
|
|
91
|
+
|
|
92
|
+
// Add rate limit headers
|
|
93
|
+
const headers = {
|
|
94
|
+
'X-RateLimit-Limit': config.maxRequests.toString(),
|
|
95
|
+
'X-RateLimit-Remaining': Math.max(0, config.maxRequests - entry.count).toString(),
|
|
96
|
+
'X-RateLimit-Reset': Math.ceil(entry.resetTime / 1000).toString()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check if rate limit exceeded
|
|
100
|
+
if (entry.count > config.maxRequests) {
|
|
101
|
+
return new Response(
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
error: config.message || 'Too many requests',
|
|
104
|
+
retryAfter: Math.ceil((entry.resetTime - Date.now()) / 1000)
|
|
105
|
+
}),
|
|
106
|
+
{
|
|
107
|
+
status: 429,
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
'Retry-After': Math.ceil((entry.resetTime - Date.now()) / 1000).toString(),
|
|
111
|
+
...headers
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Add headers to successful responses
|
|
118
|
+
context.set.headers = { ...context.set.headers, ...headers }
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Default key generator (IP-based)
|
|
124
|
+
*/
|
|
125
|
+
function getDefaultKey(context: Context): string {
|
|
126
|
+
// Try to get real IP from various headers
|
|
127
|
+
const forwarded = context.headers['x-forwarded-for']
|
|
128
|
+
const realIp = context.headers['x-real-ip']
|
|
129
|
+
const cfConnectingIp = context.headers['cf-connecting-ip']
|
|
130
|
+
|
|
131
|
+
let ip = 'unknown'
|
|
132
|
+
|
|
133
|
+
if (forwarded) {
|
|
134
|
+
ip = forwarded.split(',')[0].trim()
|
|
135
|
+
} else if (realIp) {
|
|
136
|
+
ip = realIp
|
|
137
|
+
} else if (cfConnectingIp) {
|
|
138
|
+
ip = cfConnectingIp
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return `rate_limit:${ip}`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* User-based key generator
|
|
146
|
+
*/
|
|
147
|
+
export const userKeyGenerator = (context: any): string => {
|
|
148
|
+
const userId = context.user?.id
|
|
149
|
+
return userId ? `rate_limit:user:${userId}` : getDefaultKey(context)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Endpoint-based key generator
|
|
154
|
+
*/
|
|
155
|
+
export const endpointKeyGenerator = (context: Context): string => {
|
|
156
|
+
const ip = getDefaultKey(context)
|
|
157
|
+
const path = context.path
|
|
158
|
+
return `${ip}:${path}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Common rate limit configurations
|
|
163
|
+
*/
|
|
164
|
+
export const rateLimitConfigs = {
|
|
165
|
+
// General API rate limit
|
|
166
|
+
general: {
|
|
167
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
168
|
+
maxRequests: 100,
|
|
169
|
+
message: 'Too many requests from this IP, please try again later'
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Strict rate limit for authentication endpoints
|
|
173
|
+
auth: {
|
|
174
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
175
|
+
maxRequests: 5,
|
|
176
|
+
message: 'Too many authentication attempts, please try again later'
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Lenient rate limit for public endpoints
|
|
180
|
+
public: {
|
|
181
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
182
|
+
maxRequests: 1000,
|
|
183
|
+
message: 'Rate limit exceeded for public API'
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Per-user rate limit
|
|
187
|
+
perUser: {
|
|
188
|
+
windowMs: 60 * 1000, // 1 minute
|
|
189
|
+
maxRequests: 60,
|
|
190
|
+
keyGenerator: userKeyGenerator,
|
|
191
|
+
message: 'Too many requests, please slow down'
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Logging Middleware
|
|
3
|
+
* Logs HTTP requests with timing and context information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Context } from 'elysia'
|
|
7
|
+
|
|
8
|
+
export interface RequestLogConfig {
|
|
9
|
+
logLevel?: 'debug' | 'info' | 'warn' | 'error'
|
|
10
|
+
includeBody?: boolean
|
|
11
|
+
includeHeaders?: boolean
|
|
12
|
+
excludePaths?: string[]
|
|
13
|
+
excludeHeaders?: string[]
|
|
14
|
+
maxBodyLength?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Request logging middleware
|
|
19
|
+
*/
|
|
20
|
+
export const requestLoggingMiddleware = (config: RequestLogConfig = {}) => ({
|
|
21
|
+
name: 'request-logging',
|
|
22
|
+
|
|
23
|
+
beforeHandle: async (context: Context) => {
|
|
24
|
+
// Skip logging for excluded paths
|
|
25
|
+
if (config.excludePaths?.includes(context.path)) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const startTime = Date.now()
|
|
30
|
+
|
|
31
|
+
// Store start time for duration calculation
|
|
32
|
+
context.store = { ...context.store, startTime }
|
|
33
|
+
|
|
34
|
+
// Log request start
|
|
35
|
+
const requestId = generateRequestId()
|
|
36
|
+
context.store = { ...context.store, requestId }
|
|
37
|
+
|
|
38
|
+
const logData: any = {
|
|
39
|
+
requestId,
|
|
40
|
+
method: context.request.method,
|
|
41
|
+
path: context.path,
|
|
42
|
+
query: context.query,
|
|
43
|
+
userAgent: context.headers['user-agent'],
|
|
44
|
+
ip: getClientIp(context),
|
|
45
|
+
timestamp: new Date().toISOString()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Include headers if configured
|
|
49
|
+
if (config.includeHeaders) {
|
|
50
|
+
const headers = { ...context.headers }
|
|
51
|
+
|
|
52
|
+
// Remove sensitive headers
|
|
53
|
+
const excludeHeaders = config.excludeHeaders || [
|
|
54
|
+
'authorization',
|
|
55
|
+
'cookie',
|
|
56
|
+
'x-api-key'
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
excludeHeaders.forEach(header => {
|
|
60
|
+
delete headers[header.toLowerCase()]
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
logData.headers = headers
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Include body if configured
|
|
67
|
+
if (config.includeBody && context.body) {
|
|
68
|
+
let body = context.body
|
|
69
|
+
|
|
70
|
+
// Truncate large bodies
|
|
71
|
+
if (config.maxBodyLength && typeof body === 'string') {
|
|
72
|
+
if (body.length > config.maxBodyLength) {
|
|
73
|
+
body = body.substring(0, config.maxBodyLength) + '...[truncated]'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logData.body = body
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log('📥 Request started', logData)
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
afterHandle: async (context: Context, response: Response) => {
|
|
84
|
+
const startTime = context.store?.startTime
|
|
85
|
+
const requestId = context.store?.requestId
|
|
86
|
+
|
|
87
|
+
if (!startTime) return
|
|
88
|
+
|
|
89
|
+
const duration = Date.now() - startTime
|
|
90
|
+
|
|
91
|
+
const logData: any = {
|
|
92
|
+
requestId,
|
|
93
|
+
method: context.request.method,
|
|
94
|
+
path: context.path,
|
|
95
|
+
status: response.status,
|
|
96
|
+
duration: `${duration}ms`,
|
|
97
|
+
timestamp: new Date().toISOString()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Determine log level based on status code
|
|
101
|
+
let logLevel = config.logLevel || 'info'
|
|
102
|
+
|
|
103
|
+
if (response.status >= 500) {
|
|
104
|
+
logLevel = 'error'
|
|
105
|
+
} else if (response.status >= 400) {
|
|
106
|
+
logLevel = 'warn'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add performance warning for slow requests
|
|
110
|
+
if (duration > 1000) {
|
|
111
|
+
logData.warning = 'Slow request detected'
|
|
112
|
+
logLevel = 'warn'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const logMessage = `📤 Request completed - ${context.request.method} ${context.path} ${response.status} (${duration}ms)`
|
|
116
|
+
|
|
117
|
+
switch (logLevel) {
|
|
118
|
+
case 'error':
|
|
119
|
+
console.error(logMessage, logData)
|
|
120
|
+
break
|
|
121
|
+
case 'warn':
|
|
122
|
+
console.warn(logMessage, logData)
|
|
123
|
+
break
|
|
124
|
+
case 'debug':
|
|
125
|
+
console.debug(logMessage, logData)
|
|
126
|
+
break
|
|
127
|
+
default:
|
|
128
|
+
console.log(logMessage, logData)
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
onError: async (context: Context, error: Error) => {
|
|
133
|
+
const startTime = context.store?.startTime
|
|
134
|
+
const requestId = context.store?.requestId
|
|
135
|
+
const duration = startTime ? Date.now() - startTime : 0
|
|
136
|
+
|
|
137
|
+
const logData = {
|
|
138
|
+
requestId,
|
|
139
|
+
method: context.request.method,
|
|
140
|
+
path: context.path,
|
|
141
|
+
error: error.message,
|
|
142
|
+
stack: error.stack,
|
|
143
|
+
duration: `${duration}ms`,
|
|
144
|
+
timestamp: new Date().toISOString()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.error('💥 Request failed', logData)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate unique request ID
|
|
153
|
+
*/
|
|
154
|
+
function generateRequestId(): string {
|
|
155
|
+
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get client IP address
|
|
160
|
+
*/
|
|
161
|
+
function getClientIp(context: Context): string {
|
|
162
|
+
// Try to get real IP from various headers
|
|
163
|
+
const forwarded = context.headers['x-forwarded-for']
|
|
164
|
+
const realIp = context.headers['x-real-ip']
|
|
165
|
+
const cfConnectingIp = context.headers['cf-connecting-ip']
|
|
166
|
+
|
|
167
|
+
if (forwarded) {
|
|
168
|
+
return forwarded.split(',')[0].trim()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (realIp) {
|
|
172
|
+
return realIp
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (cfConnectingIp) {
|
|
176
|
+
return cfConnectingIp
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return 'unknown'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Predefined configurations
|
|
184
|
+
*/
|
|
185
|
+
export const requestLoggingConfigs = {
|
|
186
|
+
// Development configuration - verbose logging
|
|
187
|
+
development: {
|
|
188
|
+
logLevel: 'debug' as const,
|
|
189
|
+
includeBody: true,
|
|
190
|
+
includeHeaders: true,
|
|
191
|
+
maxBodyLength: 1000,
|
|
192
|
+
excludeHeaders: ['authorization', 'cookie']
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// Production configuration - minimal logging
|
|
196
|
+
production: {
|
|
197
|
+
logLevel: 'info' as const,
|
|
198
|
+
includeBody: false,
|
|
199
|
+
includeHeaders: false,
|
|
200
|
+
excludePaths: ['/health', '/metrics']
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// Security-focused configuration
|
|
204
|
+
security: {
|
|
205
|
+
logLevel: 'warn' as const,
|
|
206
|
+
includeBody: false,
|
|
207
|
+
includeHeaders: true,
|
|
208
|
+
excludeHeaders: [
|
|
209
|
+
'authorization',
|
|
210
|
+
'cookie',
|
|
211
|
+
'x-api-key',
|
|
212
|
+
'x-auth-token'
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
}
|