create-fluxstack 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/server/backend-only.ts +5 -5
- package/app/server/index.ts +63 -54
- package/app/server/live/FluxStackConfig.ts +43 -39
- package/app/server/live/SystemMonitorIntegration.ts +2 -2
- package/app/server/live/register-components.ts +1 -1
- package/app/server/middleware/errorHandling.ts +6 -4
- package/app/server/routes/config.ts +145 -0
- package/app/server/routes/index.ts +5 -3
- package/config/app.config.ts +113 -0
- package/config/build.config.ts +24 -0
- package/config/database.config.ts +99 -0
- package/config/index.ts +68 -0
- package/config/logger.config.ts +27 -0
- package/config/runtime.config.ts +92 -0
- package/config/server.config.ts +46 -0
- package/config/services.config.ts +130 -0
- package/config/system.config.ts +105 -0
- package/core/build/index.ts +10 -4
- package/core/cli/index.ts +29 -12
- package/core/config/env.ts +37 -95
- package/core/config/runtime-config.ts +61 -58
- package/core/config/schema.ts +4 -0
- package/core/framework/server.ts +22 -10
- package/core/plugins/built-in/index.ts +7 -17
- package/core/plugins/built-in/swagger/index.ts +228 -228
- package/core/plugins/built-in/vite/index.ts +374 -358
- package/core/plugins/dependency-manager.ts +5 -5
- package/core/plugins/manager.ts +12 -12
- package/core/plugins/registry.ts +3 -3
- package/core/server/index.ts +0 -1
- package/core/server/live/ComponentRegistry.ts +34 -8
- package/core/server/live/LiveComponentPerformanceMonitor.ts +1 -1
- package/core/server/live/websocket-plugin.ts +434 -434
- package/core/server/middleware/README.md +488 -0
- package/core/server/middleware/elysia-helpers.ts +227 -0
- package/core/server/middleware/index.ts +25 -9
- package/core/server/plugins/static-files-plugin.ts +231 -231
- package/core/utils/config-schema.ts +484 -0
- package/core/utils/env.ts +306 -0
- package/core/utils/helpers.ts +4 -4
- package/core/utils/logger/colors.ts +114 -0
- package/core/utils/logger/config.ts +35 -0
- package/core/utils/logger/formatter.ts +82 -0
- package/core/utils/logger/group-logger.ts +101 -0
- package/core/utils/logger/index.ts +199 -250
- package/core/utils/logger/stack-trace.ts +92 -0
- package/core/utils/logger/startup-banner.ts +92 -0
- package/core/utils/logger/winston-logger.ts +152 -0
- package/core/utils/version.ts +5 -0
- package/create-fluxstack.ts +1 -0
- package/fluxstack.config.ts +2 -2
- package/package.json +117 -115
- package/core/config/env-dynamic.ts +0 -326
- package/core/plugins/built-in/logger/index.ts +0 -180
- package/core/server/plugins/logger.ts +0 -47
- package/core/utils/env-runtime-v2.ts +0 -232
- package/core/utils/env-runtime.ts +0 -259
- package/core/utils/logger/formatters.ts +0 -222
- package/core/utils/logger/middleware.ts +0 -253
- package/core/utils/logger/performance.ts +0 -384
- package/core/utils/logger/transports.ts +0 -365
- package/core/utils/logger.ts +0 -106
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
# Elysia Middleware Helpers
|
|
2
|
+
|
|
3
|
+
Utilitários para simplificar a criação de middlewares Elysia no FluxStack.
|
|
4
|
+
|
|
5
|
+
## 📚 Índice
|
|
6
|
+
|
|
7
|
+
- [Instalação](#instalação)
|
|
8
|
+
- [Helpers Disponíveis](#helpers-disponíveis)
|
|
9
|
+
- [createMiddleware](#createmiddleware)
|
|
10
|
+
- [createDerive](#createderive)
|
|
11
|
+
- [createGuard](#createguard)
|
|
12
|
+
- [createRateLimit](#createratelimit)
|
|
13
|
+
- [composeMiddleware](#composemiddleware)
|
|
14
|
+
- [Exemplos Práticos](#exemplos-práticos)
|
|
15
|
+
- [Comparação: Antes vs Depois](#comparação-antes-vs-depois)
|
|
16
|
+
|
|
17
|
+
## 🚀 Instalação
|
|
18
|
+
|
|
19
|
+
Os helpers já estão disponíveis no core do FluxStack:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import {
|
|
23
|
+
createMiddleware,
|
|
24
|
+
createDerive,
|
|
25
|
+
createGuard,
|
|
26
|
+
createRateLimit,
|
|
27
|
+
composeMiddleware,
|
|
28
|
+
isDevelopment
|
|
29
|
+
} from '../../../core/server/middleware'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 🛠️ Helpers Disponíveis
|
|
33
|
+
|
|
34
|
+
### `createMiddleware`
|
|
35
|
+
|
|
36
|
+
Cria um middleware Elysia simples que pode bloquear ou adicionar ao contexto.
|
|
37
|
+
|
|
38
|
+
**Parâmetros:**
|
|
39
|
+
```typescript
|
|
40
|
+
interface MiddlewareOptions<TContext = any> {
|
|
41
|
+
name: string // Nome único do middleware
|
|
42
|
+
handler: (context: TContext) // Handler que roda antes da rota
|
|
43
|
+
=> void | any // Retorne algo para bloquear
|
|
44
|
+
nonBlocking?: boolean // Use derive() em vez de onBeforeHandle()
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Exemplo:**
|
|
49
|
+
```typescript
|
|
50
|
+
const addRequestId = createMiddleware({
|
|
51
|
+
name: 'requestId',
|
|
52
|
+
handler: ({ store }) => {
|
|
53
|
+
Object.assign(store, { requestId: crypto.randomUUID() })
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
app.use(addRequestId)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `createDerive`
|
|
61
|
+
|
|
62
|
+
Cria um middleware que **adiciona** dados ao contexto sem bloquear a execução.
|
|
63
|
+
|
|
64
|
+
**Exemplo:**
|
|
65
|
+
```typescript
|
|
66
|
+
const addTimestamp = createDerive({
|
|
67
|
+
name: 'timestamp',
|
|
68
|
+
derive: () => ({
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
requestTime: new Date().toISOString()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
app
|
|
75
|
+
.use(addTimestamp)
|
|
76
|
+
.get('/', ({ timestamp, requestTime }) => ({
|
|
77
|
+
timestamp,
|
|
78
|
+
requestTime
|
|
79
|
+
}))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `createGuard`
|
|
83
|
+
|
|
84
|
+
Cria um middleware de validação/proteção que bloqueia se uma condição não for atendida.
|
|
85
|
+
|
|
86
|
+
**Exemplo:**
|
|
87
|
+
```typescript
|
|
88
|
+
const requireAuth = createGuard({
|
|
89
|
+
name: 'authGuard',
|
|
90
|
+
check: ({ store }) => {
|
|
91
|
+
// Retorne true para permitir, false para bloquear
|
|
92
|
+
return store.user?.isAuthenticated === true
|
|
93
|
+
},
|
|
94
|
+
onFail: (set) => {
|
|
95
|
+
set.status = 401
|
|
96
|
+
return { error: 'Unauthorized' }
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
app.use(requireAuth).get('/protected', () => 'Secret data')
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Exemplo Assíncrono:**
|
|
104
|
+
```typescript
|
|
105
|
+
const requireEmailVerified = createGuard({
|
|
106
|
+
name: 'emailVerified',
|
|
107
|
+
check: async ({ store }) => {
|
|
108
|
+
const user = store.user
|
|
109
|
+
if (!user) return false
|
|
110
|
+
|
|
111
|
+
// Pode fazer queries assíncronas
|
|
112
|
+
const dbUser = await User.findById(user.id)
|
|
113
|
+
return dbUser?.isEmailVerified === true
|
|
114
|
+
},
|
|
115
|
+
onFail: (set) => {
|
|
116
|
+
set.status = 403
|
|
117
|
+
return { error: 'Email verification required' }
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `createRateLimit`
|
|
123
|
+
|
|
124
|
+
Cria um middleware de rate limiting com janela deslizante.
|
|
125
|
+
|
|
126
|
+
**Parâmetros:**
|
|
127
|
+
```typescript
|
|
128
|
+
{
|
|
129
|
+
name: string // Nome do middleware
|
|
130
|
+
maxRequests: number // Máximo de requests na janela
|
|
131
|
+
windowMs: number // Tamanho da janela em ms
|
|
132
|
+
keyGenerator?: Function // Como gerar chave única (default: IP)
|
|
133
|
+
message?: string // Mensagem de erro customizada
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Exemplo:**
|
|
138
|
+
```typescript
|
|
139
|
+
const apiRateLimit = createRateLimit({
|
|
140
|
+
name: 'apiLimit',
|
|
141
|
+
maxRequests: 100,
|
|
142
|
+
windowMs: 60000, // 1 minuto
|
|
143
|
+
keyGenerator: ({ request }) =>
|
|
144
|
+
request.headers.get('x-api-key') || 'anonymous',
|
|
145
|
+
message: 'API rate limit exceeded'
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
app.use(apiRateLimit)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Exemplo com ambiente:**
|
|
152
|
+
```typescript
|
|
153
|
+
import { isDevelopment } from '../../../core/server/middleware'
|
|
154
|
+
|
|
155
|
+
const loginRateLimit = createRateLimit({
|
|
156
|
+
name: 'loginLimit',
|
|
157
|
+
maxRequests: isDevelopment() ? 100 : 5,
|
|
158
|
+
windowMs: isDevelopment() ? 60000 : 900000, // 1min dev, 15min prod
|
|
159
|
+
keyGenerator: ({ request }) => {
|
|
160
|
+
const ip = request.headers.get('x-forwarded-for') || 'unknown'
|
|
161
|
+
return `login:${ip}`
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `composeMiddleware`
|
|
167
|
+
|
|
168
|
+
Combina múltiplos middlewares em um único plugin reutilizável.
|
|
169
|
+
|
|
170
|
+
**Exemplo:**
|
|
171
|
+
```typescript
|
|
172
|
+
const protectedAdminRoute = composeMiddleware({
|
|
173
|
+
name: 'protectedAdmin',
|
|
174
|
+
middlewares: [
|
|
175
|
+
requireAuth(),
|
|
176
|
+
requireEmailVerified(),
|
|
177
|
+
requireAdmin(),
|
|
178
|
+
apiRateLimit()
|
|
179
|
+
]
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Use em várias rotas
|
|
183
|
+
app
|
|
184
|
+
.use(protectedAdminRoute)
|
|
185
|
+
.get('/admin/users', () => getUsers())
|
|
186
|
+
.delete('/admin/users/:id', ({ params }) => deleteUser(params.id))
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 💡 Exemplos Práticos
|
|
190
|
+
|
|
191
|
+
### 1. Sistema de Autenticação Completo
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// Auth obrigatório
|
|
195
|
+
export const auth = () =>
|
|
196
|
+
createGuard({
|
|
197
|
+
name: 'auth',
|
|
198
|
+
check: async ({ headers, store }) => {
|
|
199
|
+
const token = headers.authorization?.replace('Bearer ', '')
|
|
200
|
+
if (!token) return false
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const payload = jwt.verify(token)
|
|
204
|
+
const user = await User.findById(payload.userId)
|
|
205
|
+
|
|
206
|
+
if (!user?.isActive) return false
|
|
207
|
+
|
|
208
|
+
Object.assign(store, { user, token })
|
|
209
|
+
return true
|
|
210
|
+
} catch {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
onFail: (set) => {
|
|
215
|
+
set.status = 401
|
|
216
|
+
return { error: 'Authentication required' }
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// Auth opcional
|
|
221
|
+
export const optionalAuth = () =>
|
|
222
|
+
createDerive({
|
|
223
|
+
name: 'optionalAuth',
|
|
224
|
+
derive: async ({ headers }) => {
|
|
225
|
+
const token = headers.authorization?.replace('Bearer ', '')
|
|
226
|
+
if (!token) return { user: null }
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const payload = jwt.verify(token)
|
|
230
|
+
const user = await User.findById(payload.userId)
|
|
231
|
+
return { user: user?.isActive ? user : null }
|
|
232
|
+
} catch {
|
|
233
|
+
return { user: null }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 2. Validação de Permissões de Sala
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
export const requireRoomAccess = (paramName = 'roomId') =>
|
|
243
|
+
createGuard({
|
|
244
|
+
name: 'roomAccess',
|
|
245
|
+
check: async ({ store, params }) => {
|
|
246
|
+
const user = store.user
|
|
247
|
+
const roomId = params[paramName]
|
|
248
|
+
|
|
249
|
+
if (!user || !roomId) return false
|
|
250
|
+
|
|
251
|
+
const room = await Room.findById(roomId)
|
|
252
|
+
if (!room) return false
|
|
253
|
+
|
|
254
|
+
const hasAccess = room.participants.some(
|
|
255
|
+
p => p.userId.toString() === user.id
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if (hasAccess) {
|
|
259
|
+
Object.assign(store, { room })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return hasAccess
|
|
263
|
+
},
|
|
264
|
+
onFail: (set, { params }) => {
|
|
265
|
+
if (!params[paramName]) {
|
|
266
|
+
set.status = 400
|
|
267
|
+
return { error: 'Room ID required' }
|
|
268
|
+
}
|
|
269
|
+
set.status = 403
|
|
270
|
+
return { error: 'Room access denied' }
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 3. Logging e Métricas
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const requestLogger = createDerive({
|
|
279
|
+
name: 'logger',
|
|
280
|
+
derive: ({ request, store }) => {
|
|
281
|
+
const startTime = Date.now()
|
|
282
|
+
const requestId = crypto.randomUUID()
|
|
283
|
+
|
|
284
|
+
console.log(`[${requestId}] ${request.method} ${request.url}`)
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
requestId,
|
|
288
|
+
startTime,
|
|
289
|
+
logEnd: () => {
|
|
290
|
+
const duration = Date.now() - startTime
|
|
291
|
+
console.log(`[${requestId}] Completed in ${duration}ms`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
app.use(requestLogger).get('/', ({ logEnd }) => {
|
|
298
|
+
const result = doWork()
|
|
299
|
+
logEnd() // Log duration
|
|
300
|
+
return result
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 4. Validação de Roles
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
const requireRole = (...roles: string[]) =>
|
|
308
|
+
createGuard({
|
|
309
|
+
name: `requireRole:${roles.join(',')}`,
|
|
310
|
+
check: ({ store }) => {
|
|
311
|
+
const user = store.user
|
|
312
|
+
if (!user?.roles) return false
|
|
313
|
+
return roles.some(role => user.roles.includes(role))
|
|
314
|
+
},
|
|
315
|
+
onFail: (set) => {
|
|
316
|
+
set.status = 403
|
|
317
|
+
return {
|
|
318
|
+
error: 'Insufficient permissions',
|
|
319
|
+
required: roles
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// Uso
|
|
325
|
+
app
|
|
326
|
+
.use(requireRole('admin', 'moderator'))
|
|
327
|
+
.delete('/posts/:id', deletePost)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## 🔄 Comparação: Antes vs Depois
|
|
331
|
+
|
|
332
|
+
### ❌ Antes (Verboso)
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
export const auth = () =>
|
|
336
|
+
new Elysia({ name: 'auth' })
|
|
337
|
+
.onBeforeHandle(async ({ headers, set, store }) => {
|
|
338
|
+
const authHeader = headers.authorization
|
|
339
|
+
if (!authHeader) {
|
|
340
|
+
set.status = 401
|
|
341
|
+
return {
|
|
342
|
+
success: false,
|
|
343
|
+
error: 'Authorization header missing',
|
|
344
|
+
message: 'Authorization header missing'
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const token = jwtService.extractTokenFromAuthHeader(authHeader)
|
|
349
|
+
if (!token) {
|
|
350
|
+
set.status = 401
|
|
351
|
+
return {
|
|
352
|
+
success: false,
|
|
353
|
+
error: 'Invalid authorization format',
|
|
354
|
+
message: 'Invalid authorization format. Expected: Bearer <token>'
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let payload
|
|
359
|
+
try {
|
|
360
|
+
payload = jwtService.verifyAccessToken(token)
|
|
361
|
+
} catch (err) {
|
|
362
|
+
set.status = 401
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
error: 'Invalid token',
|
|
366
|
+
message: err instanceof Error ? err.message : 'Invalid token'
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const userDoc = await User.findById(payload.userId)
|
|
371
|
+
if (!userDoc) {
|
|
372
|
+
set.status = 401
|
|
373
|
+
return {
|
|
374
|
+
success: false,
|
|
375
|
+
error: 'User not found',
|
|
376
|
+
message: 'User not found'
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
Object.assign(store, { user: userDoc, token })
|
|
381
|
+
})
|
|
382
|
+
.as('plugin')
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### ✅ Depois (Limpo e Direto)
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
export const auth = () =>
|
|
389
|
+
createGuard({
|
|
390
|
+
name: 'auth',
|
|
391
|
+
check: async ({ headers, store }) => {
|
|
392
|
+
const token = headers.authorization?.replace('Bearer ', '')
|
|
393
|
+
if (!token) return false
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const payload = jwtService.verifyAccessToken(token)
|
|
397
|
+
const user = await User.findById(payload.userId)
|
|
398
|
+
|
|
399
|
+
if (!user) return false
|
|
400
|
+
|
|
401
|
+
Object.assign(store, { user, token })
|
|
402
|
+
return true
|
|
403
|
+
} catch {
|
|
404
|
+
return false
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
onFail: (set) => {
|
|
408
|
+
set.status = 401
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
error: 'Authentication required'
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Benefícios:
|
|
418
|
+
- ✅ **50% menos código**
|
|
419
|
+
- ✅ **Mais legível** - lógica de validação separada da resposta de erro
|
|
420
|
+
- ✅ **Mais reutilizável** - padrão consistente
|
|
421
|
+
- ✅ **Type-safe** - TypeScript infere tipos automaticamente
|
|
422
|
+
- ✅ **Menos bugs** - menos código boilerplate manual
|
|
423
|
+
|
|
424
|
+
## 🎯 Boas Práticas
|
|
425
|
+
|
|
426
|
+
### 1. Nomeie seus middlewares descritivamente
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// ❌ Ruim
|
|
430
|
+
const m = createMiddleware({ name: 'm', ... })
|
|
431
|
+
|
|
432
|
+
// ✅ Bom
|
|
433
|
+
const requireEmailVerification = createMiddleware({
|
|
434
|
+
name: 'emailVerification',
|
|
435
|
+
...
|
|
436
|
+
})
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### 2. Use composição para rotas complexas
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// ✅ Crie middlewares compostos reutilizáveis
|
|
443
|
+
const protectedRoute = composeMiddleware({
|
|
444
|
+
name: 'protected',
|
|
445
|
+
middlewares: [auth(), rateLimit()]
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const adminRoute = composeMiddleware({
|
|
449
|
+
name: 'admin',
|
|
450
|
+
middlewares: [protectedRoute(), requireAdmin()]
|
|
451
|
+
})
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### 3. Use helpers de ambiente
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { isDevelopment, isProduction } from '../../../core/server/middleware'
|
|
458
|
+
|
|
459
|
+
const debugMiddleware = createMiddleware({
|
|
460
|
+
name: 'debug',
|
|
461
|
+
handler: ({ request }) => {
|
|
462
|
+
if (isDevelopment()) {
|
|
463
|
+
console.log('Request:', request.method, request.url)
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
nonBlocking: true
|
|
467
|
+
})
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### 4. Separe validação de resposta de erro
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
// ✅ Bom - lógica clara e separada
|
|
474
|
+
const requirePremium = createGuard({
|
|
475
|
+
name: 'premium',
|
|
476
|
+
check: ({ store }) => store.user?.subscription === 'premium',
|
|
477
|
+
onFail: (set) => {
|
|
478
|
+
set.status = 402
|
|
479
|
+
return { error: 'Premium subscription required' }
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## 📖 Ver Também
|
|
485
|
+
|
|
486
|
+
- [Documentação Elysia](https://elysiajs.com)
|
|
487
|
+
- [Exemplo completo em `auth-refactored-example.ts`](../../../app/server/middleware/auth-refactored-example.ts)
|
|
488
|
+
- [Código dos helpers](./elysia-helpers.ts)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elysia Middleware Helpers
|
|
3
|
+
* Utilities to simplify middleware creation for FluxStack apps using Elysia
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Elysia } from 'elysia'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for creating middleware
|
|
10
|
+
*/
|
|
11
|
+
export interface MiddlewareOptions<TContext = any> {
|
|
12
|
+
/** Unique name for the middleware (required for Elysia) */
|
|
13
|
+
name: string
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Handler function that runs before the route handler.
|
|
17
|
+
* Return undefined to continue, or return a response to block execution.
|
|
18
|
+
*/
|
|
19
|
+
handler: (context: TContext) => void | Promise<void> | any | Promise<any>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* If true, uses derive() instead of onBeforeHandle().
|
|
23
|
+
* Use this when you want to add data to context without blocking.
|
|
24
|
+
*/
|
|
25
|
+
nonBlocking?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a simple Elysia middleware plugin
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const myAuth = createMiddleware({
|
|
34
|
+
* name: 'myAuth',
|
|
35
|
+
* handler: ({ headers, set }) => {
|
|
36
|
+
* const token = headers.authorization
|
|
37
|
+
* if (!token) {
|
|
38
|
+
* set.status = 401
|
|
39
|
+
* return { error: 'Unauthorized' }
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* app.use(myAuth)
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function createMiddleware<TContext = any>(
|
|
48
|
+
options: MiddlewareOptions<TContext>
|
|
49
|
+
) {
|
|
50
|
+
const { name, handler, nonBlocking = false } = options
|
|
51
|
+
|
|
52
|
+
if (nonBlocking) {
|
|
53
|
+
// Non-blocking: use derive() - adds to context without stopping execution
|
|
54
|
+
return new Elysia({ name }).derive(handler as any)
|
|
55
|
+
} else {
|
|
56
|
+
// Blocking: use onBeforeHandle() - can stop execution by returning a response
|
|
57
|
+
return new Elysia({ name }).onBeforeHandle(handler as any).as('plugin')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a middleware that derives (adds) data to the context without blocking
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* const addTimestamp = createDerive({
|
|
67
|
+
* name: 'timestamp',
|
|
68
|
+
* derive: () => ({ timestamp: Date.now() })
|
|
69
|
+
* })
|
|
70
|
+
*
|
|
71
|
+
* app.use(addTimestamp).get('/', ({ timestamp }) => ({ timestamp }))
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function createDerive<TDerived extends Record<string, any>>(options: {
|
|
75
|
+
name: string
|
|
76
|
+
derive: (context: any) => TDerived | Promise<TDerived>
|
|
77
|
+
}) {
|
|
78
|
+
return new Elysia({ name: options.name }).derive(options.derive)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a guard middleware (blocking middleware that validates conditions)
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const requireAdmin = createGuard({
|
|
87
|
+
* name: 'adminGuard',
|
|
88
|
+
* check: ({ store }) => {
|
|
89
|
+
* return store.user?.isAdmin === true
|
|
90
|
+
* },
|
|
91
|
+
* onFail: (set) => {
|
|
92
|
+
* set.status = 403
|
|
93
|
+
* return { error: 'Admin access required' }
|
|
94
|
+
* }
|
|
95
|
+
* })
|
|
96
|
+
*
|
|
97
|
+
* app.use(requireAdmin).get('/admin', () => 'Admin panel')
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function createGuard<TContext = any>(options: {
|
|
101
|
+
name: string
|
|
102
|
+
check: (context: TContext) => boolean | Promise<boolean>
|
|
103
|
+
onFail: (set: any, context: TContext) => any
|
|
104
|
+
}) {
|
|
105
|
+
return new Elysia({ name: options.name })
|
|
106
|
+
.onBeforeHandle(async (ctx) => {
|
|
107
|
+
const passed = await options.check(ctx as TContext)
|
|
108
|
+
if (!passed) {
|
|
109
|
+
return options.onFail((ctx as any).set, ctx as TContext)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
.as('plugin')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a rate limiter middleware
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const apiRateLimit = createRateLimit({
|
|
121
|
+
* name: 'apiRateLimit',
|
|
122
|
+
* maxRequests: 100,
|
|
123
|
+
* windowMs: 60000, // 1 minute
|
|
124
|
+
* keyGenerator: ({ request }) => request.headers.get('x-api-key') || 'anonymous'
|
|
125
|
+
* })
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function createRateLimit(options: {
|
|
129
|
+
name: string
|
|
130
|
+
maxRequests: number
|
|
131
|
+
windowMs: number
|
|
132
|
+
keyGenerator?: (context: any) => string
|
|
133
|
+
message?: string
|
|
134
|
+
}) {
|
|
135
|
+
const {
|
|
136
|
+
name,
|
|
137
|
+
maxRequests,
|
|
138
|
+
windowMs,
|
|
139
|
+
keyGenerator = ({ request }: any) =>
|
|
140
|
+
request.headers.get('x-forwarded-for') ||
|
|
141
|
+
request.headers.get('x-real-ip') ||
|
|
142
|
+
'unknown',
|
|
143
|
+
message = 'Too many requests'
|
|
144
|
+
} = options
|
|
145
|
+
|
|
146
|
+
const requests = new Map<string, { count: number; resetTime: number }>()
|
|
147
|
+
|
|
148
|
+
// Cleanup old entries periodically
|
|
149
|
+
setInterval(() => {
|
|
150
|
+
const now = Date.now()
|
|
151
|
+
for (const [key, data] of requests.entries()) {
|
|
152
|
+
if (now > data.resetTime) {
|
|
153
|
+
requests.delete(key)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}, Math.max(windowMs, 60000)) // At least every minute
|
|
157
|
+
|
|
158
|
+
return new Elysia({ name })
|
|
159
|
+
.onBeforeHandle((ctx) => {
|
|
160
|
+
const key = keyGenerator(ctx)
|
|
161
|
+
const now = Date.now()
|
|
162
|
+
const entry = requests.get(key)
|
|
163
|
+
|
|
164
|
+
if (entry) {
|
|
165
|
+
if (now > entry.resetTime) {
|
|
166
|
+
// Reset window
|
|
167
|
+
requests.set(key, { count: 1, resetTime: now + windowMs })
|
|
168
|
+
} else if (entry.count >= maxRequests) {
|
|
169
|
+
// Rate limit exceeded
|
|
170
|
+
;(ctx as any).set.status = 429
|
|
171
|
+
return {
|
|
172
|
+
success: false,
|
|
173
|
+
error: 'RATE_LIMIT_EXCEEDED',
|
|
174
|
+
message
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// Increment count
|
|
178
|
+
entry.count++
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// First request
|
|
182
|
+
requests.set(key, { count: 1, resetTime: now + windowMs })
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
.as('plugin')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create a composable middleware that runs multiple middlewares in sequence
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```ts
|
|
193
|
+
* const protectedRoute = composeMiddleware({
|
|
194
|
+
* name: 'protectedRoute',
|
|
195
|
+
* middlewares: [auth(), requireEmailVerification(), rateLimit()]
|
|
196
|
+
* })
|
|
197
|
+
*
|
|
198
|
+
* app.use(protectedRoute).get('/protected', () => 'Protected content')
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function composeMiddleware(options: {
|
|
202
|
+
name: string
|
|
203
|
+
middlewares: Elysia[]
|
|
204
|
+
}) {
|
|
205
|
+
let composed = new Elysia({ name: options.name })
|
|
206
|
+
|
|
207
|
+
for (const middleware of options.middlewares) {
|
|
208
|
+
composed = composed.use(middleware)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return composed.as('plugin')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Environment-aware configuration helper
|
|
216
|
+
*/
|
|
217
|
+
export function isDevelopment() {
|
|
218
|
+
return process.env.NODE_ENV === 'development'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function isProduction() {
|
|
222
|
+
return process.env.NODE_ENV === 'production'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function isTest() {
|
|
226
|
+
return process.env.NODE_ENV === 'test'
|
|
227
|
+
}
|