create-fluxstack 1.7.5 → 1.8.3
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/.dockerignore +82 -0
- package/.env.example +19 -0
- package/Dockerfile +70 -0
- package/README.md +6 -3
- package/app/client/SIMPLIFICATION.md +140 -0
- package/app/client/frontend-only.ts +1 -1
- package/app/client/src/App.tsx +148 -283
- package/app/client/src/index.css +5 -20
- package/app/client/src/lib/eden-api.ts +53 -220
- package/app/client/src/main.tsx +2 -3
- package/app/server/app.ts +20 -5
- package/app/server/backend-only.ts +15 -12
- package/app/server/controllers/users.controller.ts +57 -31
- package/app/server/index.ts +86 -96
- package/app/server/live/register-components.ts +18 -7
- package/app/server/routes/env-test.ts +110 -0
- package/app/server/routes/index.ts +1 -8
- package/app/server/routes/users.routes.ts +192 -91
- package/config/app.config.ts +2 -54
- package/config/client.config.ts +95 -0
- package/config/fluxstack.config.ts +2 -2
- package/config/index.ts +57 -22
- package/config/monitoring.config.ts +114 -0
- package/config/plugins.config.ts +80 -0
- package/config/runtime.config.ts +0 -17
- package/config/server.config.ts +50 -30
- package/core/build/bundler.ts +17 -16
- package/core/build/flux-plugins-generator.ts +34 -23
- package/core/build/index.ts +32 -31
- package/core/build/live-components-generator.ts +44 -30
- package/core/build/optimizer.ts +37 -17
- package/core/cli/command-registry.ts +4 -14
- package/core/cli/commands/plugin-deps.ts +8 -8
- package/core/cli/generators/component.ts +3 -3
- package/core/cli/generators/controller.ts +4 -4
- package/core/cli/generators/index.ts +8 -8
- package/core/cli/generators/interactive.ts +4 -4
- package/core/cli/generators/plugin.ts +3 -3
- package/core/cli/generators/prompts.ts +1 -1
- package/core/cli/generators/route.ts +27 -11
- package/core/cli/generators/service.ts +5 -5
- package/core/cli/generators/template-engine.ts +1 -1
- package/core/cli/generators/types.ts +1 -1
- package/core/cli/index.ts +158 -189
- package/core/cli/plugin-discovery.ts +3 -3
- package/core/client/hooks/index.ts +2 -2
- package/core/client/hooks/state-validator.ts +1 -1
- package/core/client/hooks/useAuth.ts +1 -1
- package/core/client/hooks/useChunkedUpload.ts +1 -1
- package/core/client/hooks/useHybridLiveComponent.ts +1 -1
- package/core/client/hooks/useWebSocket.ts +1 -1
- package/core/config/env.ts +5 -1
- package/core/config/runtime-config.ts +12 -10
- package/core/config/schema.ts +33 -2
- package/core/framework/server.ts +30 -14
- package/core/framework/types.ts +2 -2
- package/core/index.ts +31 -23
- package/core/live/ComponentRegistry.ts +1 -1
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +1 -1
- package/core/plugins/built-in/live-components/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +65 -161
- package/core/plugins/built-in/static/index.ts +75 -277
- package/core/plugins/built-in/swagger/index.ts +301 -231
- package/core/plugins/built-in/vite/index.ts +342 -377
- package/core/plugins/config.ts +2 -2
- package/core/plugins/dependency-manager.ts +2 -2
- package/core/plugins/discovery.ts +1 -1
- package/core/plugins/executor.ts +2 -2
- package/core/plugins/manager.ts +19 -4
- package/core/plugins/module-resolver.ts +1 -1
- package/core/plugins/registry.ts +25 -21
- package/core/plugins/types.ts +147 -5
- package/core/server/backend-entry.ts +51 -0
- package/core/server/framework.ts +2 -2
- package/core/server/live/ComponentRegistry.ts +9 -26
- package/core/server/live/FileUploadManager.ts +1 -1
- package/core/server/live/auto-generated-components.ts +26 -0
- package/core/server/live/websocket-plugin.ts +211 -19
- package/core/server/middleware/errorHandling.ts +1 -1
- package/core/server/middleware/index.ts +4 -4
- package/core/server/plugins/database.ts +1 -2
- package/core/server/plugins/static-files-plugin.ts +259 -231
- package/core/server/plugins/swagger.ts +1 -1
- package/core/server/services/BaseService.ts +1 -1
- package/core/server/services/ServiceContainer.ts +1 -1
- package/core/server/services/index.ts +4 -4
- package/core/server/standalone.ts +16 -1
- package/core/testing/index.ts +1 -1
- package/core/testing/setup.ts +1 -1
- package/core/types/plugin.ts +6 -0
- package/core/utils/build-logger.ts +324 -0
- package/core/utils/config-schema.ts +2 -6
- package/core/utils/helpers.ts +14 -9
- package/core/utils/logger/startup-banner.ts +7 -33
- package/core/utils/regenerate-files.ts +69 -0
- package/core/utils/version.ts +6 -6
- package/create-fluxstack.ts +68 -25
- package/fluxstack.config.ts +138 -252
- package/package.json +3 -18
- package/plugins/crypto-auth/index.ts +52 -47
- package/plugins/crypto-auth/server/AuthMiddleware.ts +1 -1
- package/plugins/crypto-auth/server/middlewares/helpers.ts +16 -1
- package/vitest.config.ts +17 -26
- package/app/client/src/App.css +0 -883
- package/app/client/src/components/ErrorBoundary.tsx +0 -107
- package/app/client/src/components/ErrorDisplay.css +0 -365
- package/app/client/src/components/ErrorDisplay.tsx +0 -258
- package/app/client/src/components/FluxStackConfig.tsx +0 -1321
- package/app/client/src/components/HybridLiveCounter.tsx +0 -140
- package/app/client/src/components/LiveClock.tsx +0 -286
- package/app/client/src/components/MainLayout.tsx +0 -388
- package/app/client/src/components/SidebarNavigation.tsx +0 -391
- package/app/client/src/components/StateDemo.tsx +0 -178
- package/app/client/src/components/SystemMonitor.tsx +0 -1044
- package/app/client/src/components/UserProfile.tsx +0 -809
- package/app/client/src/hooks/useAuth.ts +0 -39
- package/app/client/src/hooks/useNotifications.ts +0 -56
- package/app/client/src/lib/errors.ts +0 -340
- package/app/client/src/lib/hooks/useErrorHandler.ts +0 -258
- package/app/client/src/lib/index.ts +0 -45
- package/app/client/src/pages/ApiDocs.tsx +0 -182
- package/app/client/src/pages/CryptoAuthPage.tsx +0 -394
- package/app/client/src/pages/Demo.tsx +0 -174
- package/app/client/src/pages/HybridLive.tsx +0 -263
- package/app/client/src/pages/Overview.tsx +0 -155
- package/app/client/src/store/README.md +0 -43
- package/app/client/src/store/index.ts +0 -16
- package/app/client/src/store/slices/uiSlice.ts +0 -151
- package/app/client/src/store/slices/userSlice.ts +0 -161
- package/app/client/src/test/README.md +0 -257
- package/app/client/src/test/setup.ts +0 -70
- package/app/client/src/test/types.ts +0 -12
- package/app/server/live/CounterComponent.ts +0 -191
- package/app/server/live/FluxStackConfig.ts +0 -534
- package/app/server/live/SidebarNavigation.ts +0 -157
- package/app/server/live/SystemMonitor.ts +0 -595
- package/app/server/live/SystemMonitorIntegration.ts +0 -151
- package/app/server/live/UserProfileComponent.ts +0 -141
- package/app/server/middleware/auth.ts +0 -136
- package/app/server/middleware/errorHandling.ts +0 -252
- package/app/server/middleware/index.ts +0 -10
- package/app/server/middleware/rateLimit.ts +0 -193
- package/app/server/middleware/requestLogging.ts +0 -215
- package/app/server/middleware/validation.ts +0 -270
- package/app/server/routes/config.ts +0 -145
- package/app/server/routes/crypto-auth-demo.routes.ts +0 -167
- package/app/server/routes/example-with-crypto-auth.routes.ts +0 -235
- package/app/server/routes/exemplo-posts.routes.ts +0 -161
- package/app/server/routes/upload.ts +0 -92
- package/app/server/services/NotificationService.ts +0 -302
- package/app/server/services/UserService.ts +0 -222
- package/app/server/services/index.ts +0 -46
- package/app/server/types/index.ts +0 -1
- package/config/build.config.ts +0 -24
|
@@ -1,193 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation Middleware
|
|
3
|
-
* Provides request validation using schemas
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Context } from 'elysia'
|
|
7
|
-
|
|
8
|
-
export interface ValidationSchema {
|
|
9
|
-
body?: Record<string, any>
|
|
10
|
-
query?: Record<string, any>
|
|
11
|
-
params?: Record<string, any>
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface ValidationError {
|
|
15
|
-
field: string
|
|
16
|
-
message: string
|
|
17
|
-
value?: any
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Create validation middleware for a specific schema
|
|
22
|
-
*/
|
|
23
|
-
export const validationMiddleware = (schema: ValidationSchema) => ({
|
|
24
|
-
name: 'validation',
|
|
25
|
-
|
|
26
|
-
beforeHandle: async (context: Context) => {
|
|
27
|
-
const errors: ValidationError[] = []
|
|
28
|
-
|
|
29
|
-
// Validate body
|
|
30
|
-
if (schema.body && context.body) {
|
|
31
|
-
const bodyErrors = validateObject(context.body, schema.body, 'body')
|
|
32
|
-
errors.push(...bodyErrors)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Validate query parameters
|
|
36
|
-
if (schema.query && context.query) {
|
|
37
|
-
const queryErrors = validateObject(context.query, schema.query, 'query')
|
|
38
|
-
errors.push(...queryErrors)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Validate path parameters
|
|
42
|
-
if (schema.params && context.params) {
|
|
43
|
-
const paramErrors = validateObject(context.params, schema.params, 'params')
|
|
44
|
-
errors.push(...paramErrors)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (errors.length > 0) {
|
|
48
|
-
return new Response(
|
|
49
|
-
JSON.stringify({
|
|
50
|
-
error: 'Validation failed',
|
|
51
|
-
details: errors
|
|
52
|
-
}),
|
|
53
|
-
{
|
|
54
|
-
status: 400,
|
|
55
|
-
headers: { 'Content-Type': 'application/json' }
|
|
56
|
-
}
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Validate an object against a schema
|
|
64
|
-
*/
|
|
65
|
-
function validateObject(
|
|
66
|
-
obj: any,
|
|
67
|
-
schema: Record<string, any>,
|
|
68
|
-
prefix: string
|
|
69
|
-
): ValidationError[] {
|
|
70
|
-
const errors: ValidationError[] = []
|
|
71
|
-
|
|
72
|
-
for (const [field, rules] of Object.entries(schema)) {
|
|
73
|
-
const value = obj[field]
|
|
74
|
-
const fieldPath = `${prefix}.${field}`
|
|
75
|
-
|
|
76
|
-
// Check required fields
|
|
77
|
-
if (rules.required && (value === undefined || value === null || value === '')) {
|
|
78
|
-
errors.push({
|
|
79
|
-
field: fieldPath,
|
|
80
|
-
message: `${field} is required`,
|
|
81
|
-
value
|
|
82
|
-
})
|
|
83
|
-
continue
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Skip validation if field is not required and not present
|
|
87
|
-
if (!rules.required && (value === undefined || value === null)) {
|
|
88
|
-
continue
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Type validation
|
|
92
|
-
if (rules.type) {
|
|
93
|
-
const typeError = validateType(value, rules.type, fieldPath)
|
|
94
|
-
if (typeError) {
|
|
95
|
-
errors.push(typeError)
|
|
96
|
-
continue
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// String validations
|
|
101
|
-
if (rules.type === 'string' && typeof value === 'string') {
|
|
102
|
-
if (rules.minLength && value.length < rules.minLength) {
|
|
103
|
-
errors.push({
|
|
104
|
-
field: fieldPath,
|
|
105
|
-
message: `${field} must be at least ${rules.minLength} characters`,
|
|
106
|
-
value
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (rules.maxLength && value.length > rules.maxLength) {
|
|
111
|
-
errors.push({
|
|
112
|
-
field: fieldPath,
|
|
113
|
-
message: `${field} must be no more than ${rules.maxLength} characters`,
|
|
114
|
-
value
|
|
115
|
-
})
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (rules.pattern && !new RegExp(rules.pattern).test(value)) {
|
|
119
|
-
errors.push({
|
|
120
|
-
field: fieldPath,
|
|
121
|
-
message: `${field} format is invalid`,
|
|
122
|
-
value
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (rules.email && !isValidEmail(value)) {
|
|
127
|
-
errors.push({
|
|
128
|
-
field: fieldPath,
|
|
129
|
-
message: `${field} must be a valid email address`,
|
|
130
|
-
value
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Number validations
|
|
136
|
-
if (rules.type === 'number' && typeof value === 'number') {
|
|
137
|
-
if (rules.min !== undefined && value < rules.min) {
|
|
138
|
-
errors.push({
|
|
139
|
-
field: fieldPath,
|
|
140
|
-
message: `${field} must be at least ${rules.min}`,
|
|
141
|
-
value
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (rules.max !== undefined && value > rules.max) {
|
|
146
|
-
errors.push({
|
|
147
|
-
field: fieldPath,
|
|
148
|
-
message: `${field} must be no more than ${rules.max}`,
|
|
149
|
-
value
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Array validations
|
|
155
|
-
if (rules.type === 'array' && Array.isArray(value)) {
|
|
156
|
-
if (rules.minItems && value.length < rules.minItems) {
|
|
157
|
-
errors.push({
|
|
158
|
-
field: fieldPath,
|
|
159
|
-
message: `${field} must have at least ${rules.minItems} items`,
|
|
160
|
-
value
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (rules.maxItems && value.length > rules.maxItems) {
|
|
165
|
-
errors.push({
|
|
166
|
-
field: fieldPath,
|
|
167
|
-
message: `${field} must have no more than ${rules.maxItems} items`,
|
|
168
|
-
value
|
|
169
|
-
})
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Enum validation
|
|
174
|
-
if (rules.enum && !rules.enum.includes(value)) {
|
|
175
|
-
errors.push({
|
|
176
|
-
field: fieldPath,
|
|
177
|
-
message: `${field} must be one of: ${rules.enum.join(', ')}`,
|
|
178
|
-
value
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return errors
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Validate value type
|
|
188
|
-
*/
|
|
189
|
-
function validateType(value: any, expectedType: string, fieldPath: string): ValidationError | null {
|
|
190
|
-
const actualType = Array.isArray(value) ? 'array' : typeof value
|
|
191
|
-
|
|
192
|
-
if (actualType !== expectedType) {
|
|
193
|
-
return {
|
|
194
|
-
field: fieldPath,
|
|
195
|
-
message: `Expected ${expectedType}, got ${actualType}`,
|
|
196
|
-
value
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return null
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Validate email format
|
|
205
|
-
*/
|
|
206
|
-
function isValidEmail(email: string): boolean {
|
|
207
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
208
|
-
return emailRegex.test(email)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Common validation schemas
|
|
213
|
-
*/
|
|
214
|
-
export const commonSchemas = {
|
|
215
|
-
createUser: {
|
|
216
|
-
body: {
|
|
217
|
-
name: {
|
|
218
|
-
type: 'string',
|
|
219
|
-
required: true,
|
|
220
|
-
minLength: 2,
|
|
221
|
-
maxLength: 100
|
|
222
|
-
},
|
|
223
|
-
email: {
|
|
224
|
-
type: 'string',
|
|
225
|
-
required: true,
|
|
226
|
-
email: true,
|
|
227
|
-
maxLength: 255
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
|
|
232
|
-
updateUser: {
|
|
233
|
-
params: {
|
|
234
|
-
id: {
|
|
235
|
-
type: 'string',
|
|
236
|
-
required: true,
|
|
237
|
-
pattern: '^\\d+$'
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
body: {
|
|
241
|
-
name: {
|
|
242
|
-
type: 'string',
|
|
243
|
-
required: false,
|
|
244
|
-
minLength: 2,
|
|
245
|
-
maxLength: 100
|
|
246
|
-
},
|
|
247
|
-
email: {
|
|
248
|
-
type: 'string',
|
|
249
|
-
required: false,
|
|
250
|
-
email: true,
|
|
251
|
-
maxLength: 255
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
pagination: {
|
|
257
|
-
query: {
|
|
258
|
-
page: {
|
|
259
|
-
type: 'string',
|
|
260
|
-
required: false,
|
|
261
|
-
pattern: '^\\d+$'
|
|
262
|
-
},
|
|
263
|
-
limit: {
|
|
264
|
-
type: 'string',
|
|
265
|
-
required: false,
|
|
266
|
-
pattern: '^\\d+$'
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|