create-fluxstack 1.12.1 → 1.14.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/LLMD/INDEX.md +8 -1
- package/LLMD/agent.md +867 -0
- package/LLMD/config/environment-vars.md +30 -0
- package/LLMD/patterns/anti-patterns.md +100 -0
- package/LLMD/reference/routing.md +39 -39
- package/LLMD/resources/live-auth.md +465 -0
- package/LLMD/resources/live-components.md +168 -26
- package/LLMD/resources/live-logging.md +220 -0
- package/LLMD/resources/live-upload.md +59 -8
- package/LLMD/resources/rest-auth.md +290 -0
- package/README.md +520 -340
- package/app/client/index.html +2 -2
- package/app/client/public/favicon.svg +46 -0
- package/app/client/src/App.tsx +13 -1
- package/app/client/src/assets/fluxstack-static.svg +46 -0
- package/app/client/src/assets/fluxstack.svg +183 -0
- package/app/client/src/components/AppLayout.tsx +139 -9
- package/app/client/src/components/BackButton.tsx +13 -13
- package/app/client/src/components/DemoPage.tsx +4 -4
- package/app/client/src/live/AuthDemo.tsx +334 -0
- package/app/client/src/live/ChatDemo.tsx +2 -2
- package/app/client/src/live/CounterDemo.tsx +12 -12
- package/app/client/src/live/FormDemo.tsx +2 -2
- package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -16
- package/app/client/src/main.tsx +13 -13
- package/app/client/src/pages/ApiTestPage.tsx +6 -6
- package/app/client/src/pages/HomePage.tsx +80 -52
- package/app/server/auth/AuthManager.ts +213 -0
- package/app/server/auth/DevAuthProvider.ts +66 -0
- package/app/server/auth/HashManager.ts +123 -0
- package/app/server/auth/JWTAuthProvider.example.ts +101 -0
- package/app/server/auth/RateLimiter.ts +106 -0
- package/app/server/auth/contracts.ts +192 -0
- package/app/server/auth/guards/SessionGuard.ts +167 -0
- package/app/server/auth/guards/TokenGuard.ts +202 -0
- package/app/server/auth/index.ts +174 -0
- package/app/server/auth/middleware.ts +163 -0
- package/app/server/auth/providers/InMemoryProvider.ts +162 -0
- package/app/server/auth/sessions/SessionManager.ts +164 -0
- package/app/server/cache/CacheManager.ts +81 -0
- package/app/server/cache/MemoryDriver.ts +112 -0
- package/app/server/cache/contracts.ts +49 -0
- package/app/server/cache/index.ts +42 -0
- package/app/server/index.ts +14 -0
- package/app/server/live/LiveAdminPanel.ts +174 -0
- package/app/server/live/LiveChat.ts +78 -77
- package/app/server/live/LiveCounter.ts +1 -0
- package/app/server/live/LiveForm.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +38 -32
- package/app/server/live/LiveProtectedChat.ts +151 -0
- package/app/server/live/LiveRoomChat.ts +1 -0
- package/app/server/live/LiveUpload.ts +1 -0
- package/app/server/live/register-components.ts +19 -19
- package/app/server/routes/auth.routes.ts +278 -0
- package/app/server/routes/index.ts +2 -0
- package/config/index.ts +8 -0
- package/config/system/auth.config.ts +49 -0
- package/config/system/runtime.config.ts +4 -0
- package/config/system/session.config.ts +33 -0
- package/core/build/optimizer.ts +235 -235
- package/core/client/LiveComponentsProvider.tsx +76 -5
- package/core/client/components/Live.tsx +17 -10
- package/core/client/components/LiveDebugger.tsx +1324 -0
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
- package/core/client/hooks/useLiveComponent.ts +58 -5
- package/core/client/hooks/useLiveDebugger.ts +392 -0
- package/core/client/index.ts +16 -1
- package/core/framework/server.ts +36 -4
- package/core/plugins/built-in/index.ts +134 -134
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +19 -8
- package/core/plugins/built-in/monitoring/index.ts +10 -3
- package/core/plugins/built-in/vite/index.ts +151 -20
- package/core/plugins/config.ts +5 -4
- package/core/plugins/discovery.ts +11 -2
- package/core/plugins/manager.ts +11 -5
- package/core/plugins/module-resolver.ts +1 -1
- package/core/plugins/registry.ts +53 -25
- package/core/server/index.ts +15 -15
- package/core/server/live/ComponentRegistry.ts +134 -50
- package/core/server/live/FileUploadManager.ts +188 -24
- package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
- package/core/server/live/LiveDebugger.ts +462 -0
- package/core/server/live/LiveLogger.ts +144 -0
- package/core/server/live/LiveRoomManager.ts +22 -5
- package/core/server/live/StateSignature.ts +704 -643
- package/core/server/live/WebSocketConnectionManager.ts +11 -10
- package/core/server/live/auth/LiveAuthContext.ts +71 -0
- package/core/server/live/auth/LiveAuthManager.ts +304 -0
- package/core/server/live/auth/index.ts +19 -0
- package/core/server/live/auth/types.ts +179 -0
- package/core/server/live/auto-generated-components.ts +8 -2
- package/core/server/live/index.ts +16 -0
- package/core/server/live/websocket-plugin.ts +323 -22
- package/core/server/plugins/static-files-plugin.ts +179 -69
- package/core/templates/create-project.ts +0 -3
- package/core/types/build.ts +219 -219
- package/core/types/plugin.ts +107 -107
- package/core/types/types.ts +278 -22
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/index.ts +5 -2
- package/core/utils/logger/startup-banner.ts +82 -82
- package/core/utils/version.ts +6 -6
- package/package.json +1 -8
- package/plugins/crypto-auth/index.ts +6 -0
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +58 -0
- package/plugins/crypto-auth/server/index.ts +24 -21
- package/rest-tests/README.md +57 -0
- package/rest-tests/auth-token.http +113 -0
- package/rest-tests/auth.http +112 -0
- package/rest-tests/rooms-token.http +69 -0
- package/rest-tests/users-token.http +62 -0
- package/.dockerignore +0 -81
- package/Dockerfile +0 -70
- package/LIVE_COMPONENTS_REVIEW.md +0 -781
- package/app/client/src/assets/react.svg +0 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - Token Guard
|
|
3
|
+
*
|
|
4
|
+
* Guard baseado em Bearer token (header Authorization).
|
|
5
|
+
* Para APIs consumidas por mobile apps, CLIs, integrações.
|
|
6
|
+
*
|
|
7
|
+
* Fluxo:
|
|
8
|
+
* 1. Client envia: Authorization: Bearer <token>
|
|
9
|
+
* 2. Guard busca user pelo token no provider
|
|
10
|
+
* 3. Se válido, user está autenticado
|
|
11
|
+
*
|
|
12
|
+
* Tokens são armazenados no cache com referência ao userId.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
Guard,
|
|
17
|
+
Authenticatable,
|
|
18
|
+
UserProvider,
|
|
19
|
+
RequestContext,
|
|
20
|
+
} from '../contracts'
|
|
21
|
+
import type { CacheDriver } from '@server/cache/contracts'
|
|
22
|
+
|
|
23
|
+
interface StoredToken {
|
|
24
|
+
userId: string | number
|
|
25
|
+
createdAt: number
|
|
26
|
+
expiresAt: number | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class TokenGuard implements Guard {
|
|
30
|
+
readonly name: string
|
|
31
|
+
private provider: UserProvider
|
|
32
|
+
private cache: CacheDriver
|
|
33
|
+
private request: RequestContext | null = null
|
|
34
|
+
private tokenTtl: number
|
|
35
|
+
|
|
36
|
+
/** Cache do usuário para a request atual */
|
|
37
|
+
private resolvedUser: Authenticatable | null | undefined = undefined
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
name: string,
|
|
41
|
+
provider: UserProvider,
|
|
42
|
+
cache: CacheDriver,
|
|
43
|
+
tokenTtlSeconds: number = 86400 // 24h default
|
|
44
|
+
) {
|
|
45
|
+
this.name = name
|
|
46
|
+
this.provider = provider
|
|
47
|
+
this.cache = cache
|
|
48
|
+
this.tokenTtl = tokenTtlSeconds
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setRequest(context: RequestContext): void {
|
|
52
|
+
this.request = context
|
|
53
|
+
this.resolvedUser = undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async user(): Promise<Authenticatable | null> {
|
|
57
|
+
if (this.resolvedUser !== undefined) {
|
|
58
|
+
return this.resolvedUser
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.resolvedUser = null
|
|
62
|
+
|
|
63
|
+
if (!this.request) return null
|
|
64
|
+
|
|
65
|
+
// 1. Extrair token do header Authorization
|
|
66
|
+
const token = this.getBearerToken()
|
|
67
|
+
if (!token) return null
|
|
68
|
+
|
|
69
|
+
// 2. Buscar token no cache
|
|
70
|
+
const tokenData = await this.cache.get<StoredToken>(`auth_token:${this.hashToken(token)}`)
|
|
71
|
+
if (!tokenData) return null
|
|
72
|
+
|
|
73
|
+
// 3. Verificar expiração
|
|
74
|
+
if (tokenData.expiresAt && Date.now() > tokenData.expiresAt) {
|
|
75
|
+
await this.cache.delete(`auth_token:${this.hashToken(token)}`)
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Buscar usuário
|
|
80
|
+
const user = await this.provider.retrieveById(tokenData.userId)
|
|
81
|
+
if (user) {
|
|
82
|
+
this.resolvedUser = user
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return this.resolvedUser
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async id(): Promise<string | number | null> {
|
|
89
|
+
const user = await this.user()
|
|
90
|
+
return user?.getAuthId() ?? null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async check(): Promise<boolean> {
|
|
94
|
+
return (await this.user()) !== null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async guest(): Promise<boolean> {
|
|
98
|
+
return (await this.user()) === null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async attempt(credentials: Record<string, unknown>): Promise<Authenticatable | null> {
|
|
102
|
+
// 1. Buscar user
|
|
103
|
+
const user = await this.provider.retrieveByCredentials(credentials)
|
|
104
|
+
if (!user) return null
|
|
105
|
+
|
|
106
|
+
// 2. Validar password
|
|
107
|
+
const valid = await this.provider.validateCredentials(user, credentials)
|
|
108
|
+
if (!valid) return null
|
|
109
|
+
|
|
110
|
+
// 3. Gerar e armazenar token
|
|
111
|
+
await this.login(user)
|
|
112
|
+
|
|
113
|
+
return user
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async login(user: Authenticatable): Promise<void> {
|
|
117
|
+
// Gerar token
|
|
118
|
+
const token = this.generateToken()
|
|
119
|
+
const hashedToken = this.hashToken(token)
|
|
120
|
+
|
|
121
|
+
// Armazenar no cache
|
|
122
|
+
const tokenData: StoredToken = {
|
|
123
|
+
userId: user.getAuthId(),
|
|
124
|
+
createdAt: Date.now(),
|
|
125
|
+
expiresAt: this.tokenTtl > 0 ? Date.now() + (this.tokenTtl * 1000) : null,
|
|
126
|
+
}
|
|
127
|
+
await this.cache.set(`auth_token:${hashedToken}`, tokenData, this.tokenTtl || undefined)
|
|
128
|
+
|
|
129
|
+
// Armazenar lista de tokens do user (para revogação em massa)
|
|
130
|
+
const userTokensKey = `user_tokens:${user.getAuthId()}`
|
|
131
|
+
const existingTokens = await this.cache.get<string[]>(userTokensKey) ?? []
|
|
132
|
+
existingTokens.push(hashedToken)
|
|
133
|
+
await this.cache.set(userTokensKey, existingTokens)
|
|
134
|
+
|
|
135
|
+
// Cache do user
|
|
136
|
+
this.resolvedUser = user
|
|
137
|
+
|
|
138
|
+
// Salvar token plain-text temporariamente para a response poder retorná-lo
|
|
139
|
+
;(this as any)._lastGeneratedToken = token
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async logout(): Promise<void> {
|
|
143
|
+
if (!this.request) return
|
|
144
|
+
|
|
145
|
+
const token = this.getBearerToken()
|
|
146
|
+
if (token) {
|
|
147
|
+
await this.cache.delete(`auth_token:${this.hashToken(token)}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.resolvedUser = null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async validate(credentials: Record<string, unknown>): Promise<boolean> {
|
|
154
|
+
const user = await this.provider.retrieveByCredentials(credentials)
|
|
155
|
+
if (!user) return false
|
|
156
|
+
return this.provider.validateCredentials(user, credentials)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Revoga todos os tokens de um usuário.
|
|
161
|
+
*/
|
|
162
|
+
async revokeAllTokens(userId: string | number): Promise<void> {
|
|
163
|
+
const userTokensKey = `user_tokens:${userId}`
|
|
164
|
+
const tokens = await this.cache.get<string[]>(userTokensKey) ?? []
|
|
165
|
+
|
|
166
|
+
for (const hashedToken of tokens) {
|
|
167
|
+
await this.cache.delete(`auth_token:${hashedToken}`)
|
|
168
|
+
}
|
|
169
|
+
await this.cache.delete(userTokensKey)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Retorna o último token gerado (para a response após login) */
|
|
173
|
+
getLastGeneratedToken(): string | null {
|
|
174
|
+
return (this as any)._lastGeneratedToken ?? null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Extrai Bearer token do header Authorization */
|
|
178
|
+
private getBearerToken(): string | null {
|
|
179
|
+
if (!this.request) return null
|
|
180
|
+
|
|
181
|
+
const authHeader = this.request.headers['authorization'] ?? this.request.headers['Authorization']
|
|
182
|
+
if (!authHeader?.startsWith('Bearer ')) return null
|
|
183
|
+
|
|
184
|
+
return authHeader.slice(7)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Gera um token criptograficamente seguro */
|
|
188
|
+
private generateToken(): string {
|
|
189
|
+
const bytes = new Uint8Array(32)
|
|
190
|
+
crypto.getRandomValues(bytes)
|
|
191
|
+
return Array.from(bytes)
|
|
192
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
193
|
+
.join('')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Hash do token para armazenamento (nunca guardar plain-text) */
|
|
197
|
+
private hashToken(token: string): string {
|
|
198
|
+
const hasher = new Bun.CryptoHasher('sha256')
|
|
199
|
+
hasher.update(token)
|
|
200
|
+
return hasher.digest('hex')
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth System
|
|
3
|
+
*
|
|
4
|
+
* Sistema de autenticação modular inspirado no Laravel.
|
|
5
|
+
* Guard + Provider pattern com session e token support.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { auth, guest, initAuth, getAuthManager } from '@server/auth'
|
|
9
|
+
*
|
|
10
|
+
* // Inicializar (no boot da app)
|
|
11
|
+
* initAuth()
|
|
12
|
+
*
|
|
13
|
+
* // Proteger rotas
|
|
14
|
+
* app.use(auth()).get('/me', ({ user }) => user.toJSON())
|
|
15
|
+
* app.use(guest()).post('/login', loginHandler)
|
|
16
|
+
*
|
|
17
|
+
* // Usar auth manager diretamente
|
|
18
|
+
* const manager = getAuthManager()
|
|
19
|
+
* const guard = manager.guard('session')
|
|
20
|
+
* const user = await guard.attempt({ email, password })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Contracts
|
|
25
|
+
export type {
|
|
26
|
+
Authenticatable,
|
|
27
|
+
Guard,
|
|
28
|
+
UserProvider,
|
|
29
|
+
RequestContext,
|
|
30
|
+
CookieOptions,
|
|
31
|
+
GuardConfig,
|
|
32
|
+
GuardFactory,
|
|
33
|
+
} from './contracts'
|
|
34
|
+
|
|
35
|
+
// Auth Manager
|
|
36
|
+
export { AuthManager } from './AuthManager'
|
|
37
|
+
export type { AuthManagerConfig, ProviderConfig } from './AuthManager'
|
|
38
|
+
|
|
39
|
+
// Hash
|
|
40
|
+
export { HashManager, Hash, getHashManager, setHashManager } from './HashManager'
|
|
41
|
+
export type { HashAlgorithm, HashOptions } from './HashManager'
|
|
42
|
+
|
|
43
|
+
// Rate Limiter
|
|
44
|
+
export { RateLimiter } from './RateLimiter'
|
|
45
|
+
|
|
46
|
+
// Guards
|
|
47
|
+
export { SessionGuard } from './guards/SessionGuard'
|
|
48
|
+
export { TokenGuard } from './guards/TokenGuard'
|
|
49
|
+
|
|
50
|
+
// Providers
|
|
51
|
+
export { InMemoryUserProvider, InMemoryUser } from './providers/InMemoryProvider'
|
|
52
|
+
|
|
53
|
+
// Sessions
|
|
54
|
+
export { SessionManager } from './sessions/SessionManager'
|
|
55
|
+
export type { SessionData, SessionConfig } from './sessions/SessionManager'
|
|
56
|
+
|
|
57
|
+
// Middleware
|
|
58
|
+
export { auth, guest, authOptional } from './middleware'
|
|
59
|
+
|
|
60
|
+
// ===== Boot =====
|
|
61
|
+
|
|
62
|
+
import { AuthManager } from './AuthManager'
|
|
63
|
+
import { HashManager, setHashManager } from './HashManager'
|
|
64
|
+
import { RateLimiter } from './RateLimiter'
|
|
65
|
+
import { SessionManager } from './sessions/SessionManager'
|
|
66
|
+
import { InMemoryUserProvider } from './providers/InMemoryProvider'
|
|
67
|
+
import { setAuthManagerForMiddleware, buildRequestContext } from './middleware'
|
|
68
|
+
import { cacheManager } from '@server/cache'
|
|
69
|
+
import { authConfig } from '@config/system/auth.config'
|
|
70
|
+
import { sessionConfig } from '@config/system/session.config'
|
|
71
|
+
|
|
72
|
+
let authManagerInstance: AuthManager | null = null
|
|
73
|
+
let rateLimiterInstance: RateLimiter | null = null
|
|
74
|
+
let sessionManagerInstance: SessionManager | null = null
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Inicializa o sistema de auth.
|
|
78
|
+
* Deve ser chamado uma vez no boot da aplicação.
|
|
79
|
+
*/
|
|
80
|
+
export function initAuth(): {
|
|
81
|
+
authManager: AuthManager
|
|
82
|
+
rateLimiter: RateLimiter
|
|
83
|
+
sessionManager: SessionManager
|
|
84
|
+
} {
|
|
85
|
+
// 1. Configurar Hash
|
|
86
|
+
const hashManager = new HashManager({
|
|
87
|
+
algorithm: authConfig.passwords.hashAlgorithm as 'bcrypt' | 'argon2id',
|
|
88
|
+
bcryptRounds: authConfig.passwords.bcryptRounds,
|
|
89
|
+
})
|
|
90
|
+
setHashManager(hashManager)
|
|
91
|
+
|
|
92
|
+
// 2. Criar Session Manager
|
|
93
|
+
const cache = cacheManager.driver()
|
|
94
|
+
sessionManagerInstance = new SessionManager(cache, {
|
|
95
|
+
lifetime: sessionConfig.lifetime,
|
|
96
|
+
cookieName: sessionConfig.cookieName,
|
|
97
|
+
httpOnly: sessionConfig.httpOnly,
|
|
98
|
+
secure: sessionConfig.secure,
|
|
99
|
+
sameSite: sessionConfig.sameSite as 'strict' | 'lax' | 'none',
|
|
100
|
+
path: sessionConfig.path,
|
|
101
|
+
domain: sessionConfig.domain || undefined,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// 3. Criar Rate Limiter
|
|
105
|
+
rateLimiterInstance = new RateLimiter(cache)
|
|
106
|
+
|
|
107
|
+
// 4. Criar Auth Manager
|
|
108
|
+
authManagerInstance = new AuthManager(
|
|
109
|
+
{
|
|
110
|
+
defaults: {
|
|
111
|
+
guard: authConfig.defaults.guard ?? 'session',
|
|
112
|
+
provider: authConfig.defaults.provider ?? 'memory',
|
|
113
|
+
},
|
|
114
|
+
guards: {
|
|
115
|
+
session: {
|
|
116
|
+
driver: 'session',
|
|
117
|
+
provider: 'memory',
|
|
118
|
+
},
|
|
119
|
+
token: {
|
|
120
|
+
driver: 'token',
|
|
121
|
+
provider: 'memory',
|
|
122
|
+
tokenTtl: authConfig.token.ttl,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
providers: {
|
|
126
|
+
memory: {
|
|
127
|
+
driver: 'memory',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
sessionManagerInstance
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// 5. Registrar InMemoryProvider como default
|
|
135
|
+
const inMemoryProvider = new InMemoryUserProvider()
|
|
136
|
+
authManagerInstance.registerProvider('memory', inMemoryProvider)
|
|
137
|
+
|
|
138
|
+
// 6. Conectar middleware
|
|
139
|
+
setAuthManagerForMiddleware(authManagerInstance)
|
|
140
|
+
|
|
141
|
+
console.log(`🔐 Auth system initialized (guard: ${authConfig.defaults.guard}, hash: ${authConfig.passwords.hashAlgorithm})`)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
authManager: authManagerInstance,
|
|
145
|
+
rateLimiter: rateLimiterInstance,
|
|
146
|
+
sessionManager: sessionManagerInstance,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Retorna o AuthManager (deve ser chamado após initAuth) */
|
|
151
|
+
export function getAuthManager(): AuthManager {
|
|
152
|
+
if (!authManagerInstance) {
|
|
153
|
+
throw new Error('Auth not initialized. Call initAuth() first.')
|
|
154
|
+
}
|
|
155
|
+
return authManagerInstance
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Retorna o RateLimiter (deve ser chamado após initAuth) */
|
|
159
|
+
export function getRateLimiter(): RateLimiter {
|
|
160
|
+
if (!rateLimiterInstance) {
|
|
161
|
+
throw new Error('Auth not initialized. Call initAuth() first.')
|
|
162
|
+
}
|
|
163
|
+
return rateLimiterInstance
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Retorna o SessionManager (deve ser chamado após initAuth) */
|
|
167
|
+
export function getSessionManager(): SessionManager {
|
|
168
|
+
if (!sessionManagerInstance) {
|
|
169
|
+
throw new Error('Auth not initialized. Call initAuth() first.')
|
|
170
|
+
}
|
|
171
|
+
return sessionManagerInstance
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { buildRequestContext }
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - Elysia Middleware
|
|
3
|
+
*
|
|
4
|
+
* Middlewares prontos para proteger rotas:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // Rota protegida (requer login)
|
|
8
|
+
* app.use(auth()).get('/me', ({ user }) => user.toJSON())
|
|
9
|
+
*
|
|
10
|
+
* // Rota de guest (requer NÃO estar logado)
|
|
11
|
+
* app.use(guest()).post('/login', loginHandler)
|
|
12
|
+
*
|
|
13
|
+
* // Guard específico
|
|
14
|
+
* app.use(auth('api')).get('/api/data', handler)
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Elysia } from 'elysia'
|
|
19
|
+
import type { AuthManager } from './AuthManager'
|
|
20
|
+
import type { Authenticatable, RequestContext } from './contracts'
|
|
21
|
+
|
|
22
|
+
/** Referência ao AuthManager (setado no boot) */
|
|
23
|
+
let authManagerRef: AuthManager | null = null
|
|
24
|
+
|
|
25
|
+
export function setAuthManagerForMiddleware(manager: AuthManager): void {
|
|
26
|
+
authManagerRef = manager
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extrai RequestContext do context do Elysia.
|
|
31
|
+
*/
|
|
32
|
+
function buildRequestContext(ctx: any): RequestContext {
|
|
33
|
+
const headers: Record<string, string | undefined> = {}
|
|
34
|
+
if (ctx.headers) {
|
|
35
|
+
for (const [key, value] of Object.entries(ctx.headers)) {
|
|
36
|
+
headers[key] = value as string | undefined
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
headers,
|
|
42
|
+
cookie: ctx.cookie ?? {},
|
|
43
|
+
setCookie: (name: string, value: string, options?: any) => {
|
|
44
|
+
if (ctx.cookie) {
|
|
45
|
+
ctx.cookie[name].set({
|
|
46
|
+
value,
|
|
47
|
+
...options,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
removeCookie: (name: string) => {
|
|
52
|
+
if (ctx.cookie) {
|
|
53
|
+
ctx.cookie[name].set({
|
|
54
|
+
value: '',
|
|
55
|
+
maxAge: 0,
|
|
56
|
+
path: '/',
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
ip: ctx.request?.headers?.get('x-forwarded-for')
|
|
61
|
+
?? ctx.request?.headers?.get('x-real-ip')
|
|
62
|
+
?? ctx.server?.requestIP?.(ctx.request)?.address
|
|
63
|
+
?? '127.0.0.1',
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Middleware que requer autenticação.
|
|
69
|
+
* Injeta `user` (Authenticatable) no context do Elysia.
|
|
70
|
+
*
|
|
71
|
+
* Retorna 401 se não autenticado.
|
|
72
|
+
*/
|
|
73
|
+
export function auth(guardName?: string) {
|
|
74
|
+
return new Elysia({ name: `auth-guard${guardName ? `-${guardName}` : ''}` })
|
|
75
|
+
.derive(async (ctx) => {
|
|
76
|
+
if (!authManagerRef) {
|
|
77
|
+
throw new Error('Auth system not initialized. Did you forget to call initAuth()?')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const requestContext = buildRequestContext(ctx)
|
|
81
|
+
const guard = authManagerRef.freshGuard(guardName, requestContext)
|
|
82
|
+
|
|
83
|
+
const user = await guard.user()
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
user: user as Authenticatable | null,
|
|
87
|
+
auth: guard,
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.onBeforeHandle(async (ctx) => {
|
|
91
|
+
if (!(ctx as any).user) {
|
|
92
|
+
(ctx as any).set.status = 401
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: 'Unauthenticated',
|
|
96
|
+
message: 'You must be logged in to access this resource.',
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.as('scoped')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Middleware que requer NÃO estar autenticado.
|
|
105
|
+
* Útil para rotas de login/register.
|
|
106
|
+
*
|
|
107
|
+
* Retorna 409 se já autenticado.
|
|
108
|
+
*/
|
|
109
|
+
export function guest(guardName?: string) {
|
|
110
|
+
return new Elysia({ name: `guest-guard${guardName ? `-${guardName}` : ''}` })
|
|
111
|
+
.derive(async (ctx) => {
|
|
112
|
+
if (!authManagerRef) {
|
|
113
|
+
throw new Error('Auth system not initialized. Did you forget to call initAuth()?')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const requestContext = buildRequestContext(ctx)
|
|
117
|
+
const guard = authManagerRef.freshGuard(guardName, requestContext)
|
|
118
|
+
|
|
119
|
+
const user = await guard.user()
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
user: user as Authenticatable | null,
|
|
123
|
+
auth: guard,
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
.onBeforeHandle(async (ctx) => {
|
|
127
|
+
if ((ctx as any).user) {
|
|
128
|
+
(ctx as any).set.status = 409
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
error: 'AlreadyAuthenticated',
|
|
132
|
+
message: 'You are already logged in.',
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.as('scoped')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Middleware que resolve auth opcionalmente (não bloqueia).
|
|
141
|
+
* Útil para rotas que funcionam com ou sem login.
|
|
142
|
+
*/
|
|
143
|
+
export function authOptional(guardName?: string) {
|
|
144
|
+
return new Elysia({ name: `auth-optional${guardName ? `-${guardName}` : ''}` })
|
|
145
|
+
.derive(async (ctx) => {
|
|
146
|
+
if (!authManagerRef) {
|
|
147
|
+
return { user: null, auth: null }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const requestContext = buildRequestContext(ctx)
|
|
151
|
+
const guard = authManagerRef.freshGuard(guardName, requestContext)
|
|
152
|
+
|
|
153
|
+
const user = await guard.user()
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
user: user as Authenticatable | null,
|
|
157
|
+
auth: guard,
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.as('scoped')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { buildRequestContext }
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - In-Memory User Provider
|
|
3
|
+
*
|
|
4
|
+
* Provider que armazena usuários em memória.
|
|
5
|
+
* Ideal para desenvolvimento, demos e testes.
|
|
6
|
+
*
|
|
7
|
+
* Para produção, implemente UserProvider com seu ORM:
|
|
8
|
+
* ```ts
|
|
9
|
+
* class DrizzleUserProvider implements UserProvider {
|
|
10
|
+
* async retrieveById(id) {
|
|
11
|
+
* return db.select().from(users).where(eq(users.id, id)).get()
|
|
12
|
+
* }
|
|
13
|
+
* // ...
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Authenticatable, UserProvider } from '../contracts'
|
|
19
|
+
import { Hash } from '../HashManager'
|
|
20
|
+
|
|
21
|
+
// ===== In-Memory User Model =====
|
|
22
|
+
|
|
23
|
+
export class InMemoryUser implements Authenticatable {
|
|
24
|
+
id: number
|
|
25
|
+
name: string
|
|
26
|
+
email: string
|
|
27
|
+
passwordHash: string
|
|
28
|
+
rememberToken: string | null = null
|
|
29
|
+
createdAt: Date
|
|
30
|
+
|
|
31
|
+
constructor(data: {
|
|
32
|
+
id: number
|
|
33
|
+
name: string
|
|
34
|
+
email: string
|
|
35
|
+
passwordHash: string
|
|
36
|
+
createdAt?: Date
|
|
37
|
+
}) {
|
|
38
|
+
this.id = data.id
|
|
39
|
+
this.name = data.name
|
|
40
|
+
this.email = data.email
|
|
41
|
+
this.passwordHash = data.passwordHash
|
|
42
|
+
this.createdAt = data.createdAt ?? new Date()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getAuthId(): number {
|
|
46
|
+
return this.id
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getAuthIdField(): string {
|
|
50
|
+
return 'id'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getAuthPassword(): string {
|
|
54
|
+
return this.passwordHash
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getRememberToken(): string | null {
|
|
58
|
+
return this.rememberToken
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setRememberToken(token: string | null): void {
|
|
62
|
+
this.rememberToken = token
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
toJSON(): Record<string, unknown> {
|
|
66
|
+
return {
|
|
67
|
+
id: this.id,
|
|
68
|
+
name: this.name,
|
|
69
|
+
email: this.email,
|
|
70
|
+
createdAt: this.createdAt.toISOString(),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ===== Provider =====
|
|
76
|
+
|
|
77
|
+
export class InMemoryUserProvider implements UserProvider {
|
|
78
|
+
private users: InMemoryUser[] = []
|
|
79
|
+
private nextId = 1
|
|
80
|
+
|
|
81
|
+
async retrieveById(id: string | number): Promise<Authenticatable | null> {
|
|
82
|
+
return this.users.find(u => u.id === Number(id)) ?? null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async retrieveByCredentials(credentials: Record<string, unknown>): Promise<Authenticatable | null> {
|
|
86
|
+
// Buscar por qualquer campo EXCETO password
|
|
87
|
+
const { password: _, ...searchFields } = credentials
|
|
88
|
+
|
|
89
|
+
return this.users.find(user => {
|
|
90
|
+
return Object.entries(searchFields).every(([key, value]) => {
|
|
91
|
+
return (user as any)[key] === value
|
|
92
|
+
})
|
|
93
|
+
}) ?? null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async validateCredentials(user: Authenticatable, credentials: Record<string, unknown>): Promise<boolean> {
|
|
97
|
+
const password = credentials.password as string | undefined
|
|
98
|
+
if (!password) return false
|
|
99
|
+
|
|
100
|
+
const valid = await Hash.check(password, user.getAuthPassword())
|
|
101
|
+
|
|
102
|
+
// Rehash transparente se necessário
|
|
103
|
+
if (valid && Hash.needsRehash(user.getAuthPassword())) {
|
|
104
|
+
const newHash = await Hash.make(password)
|
|
105
|
+
// Atualizar hash in-memory
|
|
106
|
+
const memUser = user as InMemoryUser
|
|
107
|
+
memUser.passwordHash = newHash
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return valid
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async retrieveByToken(id: string | number, token: string): Promise<Authenticatable | null> {
|
|
114
|
+
const user = this.users.find(u => u.id === Number(id))
|
|
115
|
+
if (!user) return null
|
|
116
|
+
if (user.getRememberToken() !== token) return null
|
|
117
|
+
return user
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async updateRememberToken(user: Authenticatable, token: string | null): Promise<void> {
|
|
121
|
+
user.setRememberToken(token)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async createUser(data: Record<string, unknown>): Promise<Authenticatable> {
|
|
125
|
+
const name = data.name as string
|
|
126
|
+
const email = data.email as string
|
|
127
|
+
const password = data.password as string
|
|
128
|
+
|
|
129
|
+
if (!name || !email || !password) {
|
|
130
|
+
throw new Error('Name, email and password are required')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Verificar email duplicado
|
|
134
|
+
const existing = this.users.find(u => u.email === email)
|
|
135
|
+
if (existing) {
|
|
136
|
+
throw new Error('Email already in use')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const passwordHash = await Hash.make(password)
|
|
140
|
+
|
|
141
|
+
const user = new InMemoryUser({
|
|
142
|
+
id: this.nextId++,
|
|
143
|
+
name,
|
|
144
|
+
email,
|
|
145
|
+
passwordHash,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
this.users.push(user)
|
|
149
|
+
return user
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Para testes: reset completo */
|
|
153
|
+
reset(): void {
|
|
154
|
+
this.users = []
|
|
155
|
+
this.nextId = 1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Para testes: retorna todos os usuários */
|
|
159
|
+
getAll(): InMemoryUser[] {
|
|
160
|
+
return [...this.users]
|
|
161
|
+
}
|
|
162
|
+
}
|