create-fluxstack 1.12.0 → 1.13.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/resources/live-auth.md +447 -0
- package/LLMD/resources/live-components.md +79 -21
- package/LLMD/resources/live-logging.md +158 -0
- package/LLMD/resources/live-upload.md +1 -1
- package/LLMD/resources/rest-auth.md +290 -0
- package/README.md +520 -340
- package/app/client/src/App.tsx +11 -0
- package/app/client/src/components/AppLayout.tsx +1 -0
- package/app/client/src/live/AuthDemo.tsx +332 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -105
- 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 +173 -0
- package/app/server/live/LiveCounter.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +13 -8
- package/app/server/live/LiveProtectedChat.ts +150 -0
- package/app/server/live/LiveRoomChat.ts +45 -203
- 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/session.config.ts +33 -0
- package/core/client/LiveComponentsProvider.tsx +76 -5
- package/core/client/components/Live.tsx +2 -1
- package/core/client/hooks/useLiveComponent.ts +47 -4
- package/core/client/index.ts +2 -1
- package/core/framework/server.ts +36 -4
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +15 -8
- package/core/plugins/built-in/monitoring/index.ts +10 -3
- package/core/plugins/built-in/vite/index.ts +95 -18
- 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/live/ComponentRegistry.ts +79 -24
- package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
- package/core/server/live/LiveLogger.ts +111 -0
- package/core/server/live/LiveRoomManager.ts +5 -4
- package/core/server/live/StateSignature.ts +644 -643
- 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 +92 -16
- package/core/templates/create-project.ts +0 -3
- package/core/types/types.ts +133 -13
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/index.ts +5 -2
- package/core/utils/version.ts +1 -1
- 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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth System - Contracts
|
|
3
|
+
*
|
|
4
|
+
* Inspirado no Laravel: Guard + Provider + Authenticatable.
|
|
5
|
+
* - Guard: COMO autenticar (session, token, JWT...)
|
|
6
|
+
* - UserProvider: ONDE buscar os usuários (memória, banco, API...)
|
|
7
|
+
* - Authenticatable: O QUE é um usuário autenticável
|
|
8
|
+
*
|
|
9
|
+
* Adaptado para API-only (sem HTML, sem redirects, sem CSRF).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ===== Authenticatable =====
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Interface que define o que é um "usuário autenticável".
|
|
16
|
+
* Qualquer model/entidade que implemente isso pode ser usada com o auth system.
|
|
17
|
+
*
|
|
18
|
+
* Para usar com seu ORM:
|
|
19
|
+
* ```ts
|
|
20
|
+
* class User implements Authenticatable {
|
|
21
|
+
* getAuthId() { return this.id }
|
|
22
|
+
* getAuthPassword() { return this.passwordHash }
|
|
23
|
+
* // ...
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export interface Authenticatable {
|
|
28
|
+
/** Retorna o identificador único (ex: id, uuid) */
|
|
29
|
+
getAuthId(): string | number
|
|
30
|
+
|
|
31
|
+
/** Retorna o nome do campo identificador (ex: 'id') */
|
|
32
|
+
getAuthIdField(): string
|
|
33
|
+
|
|
34
|
+
/** Retorna o hash da password armazenada */
|
|
35
|
+
getAuthPassword(): string
|
|
36
|
+
|
|
37
|
+
/** Retorna o token de "remember me" (ou null) */
|
|
38
|
+
getRememberToken(): string | null
|
|
39
|
+
|
|
40
|
+
/** Define o token de "remember me" */
|
|
41
|
+
setRememberToken(token: string | null): void
|
|
42
|
+
|
|
43
|
+
/** Retorna dados serializáveis do usuário (para response da API) */
|
|
44
|
+
toJSON(): Record<string, unknown>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ===== UserProvider =====
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Interface para buscar e validar usuários.
|
|
51
|
+
* O provider NÃO sabe como autenticar - apenas onde os dados estão.
|
|
52
|
+
*
|
|
53
|
+
* Para implementar com banco de dados:
|
|
54
|
+
* ```ts
|
|
55
|
+
* class DrizzleUserProvider implements UserProvider {
|
|
56
|
+
* async retrieveById(id) { return db.select().from(users).where(eq(users.id, id)) }
|
|
57
|
+
* async retrieveByCredentials({ email }) { return db.select().from(users).where(eq(users.email, email)) }
|
|
58
|
+
* async validateCredentials(user, { password }) { return Hash.check(password, user.getAuthPassword()) }
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export interface UserProvider {
|
|
63
|
+
/** Busca usuário pelo ID */
|
|
64
|
+
retrieveById(id: string | number): Promise<Authenticatable | null>
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Busca usuário pelas credenciais (SEM a password).
|
|
68
|
+
* Ex: { email: "user@example.com" } → busca por email.
|
|
69
|
+
* A password é validada separadamente em validateCredentials().
|
|
70
|
+
*/
|
|
71
|
+
retrieveByCredentials(credentials: Record<string, unknown>): Promise<Authenticatable | null>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Valida a password contra o hash do usuário.
|
|
75
|
+
* Recebe o user já encontrado + as credenciais originais (com password).
|
|
76
|
+
*/
|
|
77
|
+
validateCredentials(user: Authenticatable, credentials: Record<string, unknown>): Promise<boolean>
|
|
78
|
+
|
|
79
|
+
/** Busca usuário pelo remember token */
|
|
80
|
+
retrieveByToken(id: string | number, token: string): Promise<Authenticatable | null>
|
|
81
|
+
|
|
82
|
+
/** Atualiza o remember token do usuário */
|
|
83
|
+
updateRememberToken(user: Authenticatable, token: string | null): Promise<void>
|
|
84
|
+
|
|
85
|
+
/** Cria um novo usuário (para registro) */
|
|
86
|
+
createUser(data: Record<string, unknown>): Promise<Authenticatable>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ===== Guard =====
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Interface do guard de autenticação.
|
|
93
|
+
* O guard sabe COMO autenticar (session cookie, bearer token, etc).
|
|
94
|
+
*
|
|
95
|
+
* Para criar um guard customizado:
|
|
96
|
+
* ```ts
|
|
97
|
+
* class JWTGuard implements Guard {
|
|
98
|
+
* async user() { /* decode JWT from header *\/ }
|
|
99
|
+
* async attempt(creds) { /* validate + generate JWT *\/ }
|
|
100
|
+
* }
|
|
101
|
+
* authManager.extend('jwt', (config) => new JWTGuard(config))
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export interface Guard {
|
|
105
|
+
/** Nome do guard */
|
|
106
|
+
readonly name: string
|
|
107
|
+
|
|
108
|
+
/** Retorna o usuário autenticado ou null */
|
|
109
|
+
user(): Promise<Authenticatable | null>
|
|
110
|
+
|
|
111
|
+
/** Retorna o ID do usuário autenticado ou null */
|
|
112
|
+
id(): Promise<string | number | null>
|
|
113
|
+
|
|
114
|
+
/** Verifica se há um usuário autenticado */
|
|
115
|
+
check(): Promise<boolean>
|
|
116
|
+
|
|
117
|
+
/** Verifica se NÃO há usuário autenticado */
|
|
118
|
+
guest(): Promise<boolean>
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Tenta autenticar com credenciais.
|
|
122
|
+
* Retorna o usuário se sucesso, null se falha.
|
|
123
|
+
*/
|
|
124
|
+
attempt(credentials: Record<string, unknown>, remember?: boolean): Promise<Authenticatable | null>
|
|
125
|
+
|
|
126
|
+
/** Autentica um usuário diretamente (sem validar credenciais) */
|
|
127
|
+
login(user: Authenticatable, remember?: boolean): Promise<void>
|
|
128
|
+
|
|
129
|
+
/** Desloga o usuário atual */
|
|
130
|
+
logout(): Promise<void>
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Valida credenciais SEM efetuar login.
|
|
134
|
+
* Útil para confirmar password antes de ações sensíveis.
|
|
135
|
+
*/
|
|
136
|
+
validate(credentials: Record<string, unknown>): Promise<boolean>
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Inicializa o guard com o contexto da request atual.
|
|
140
|
+
* Chamado pelo middleware antes de qualquer operação.
|
|
141
|
+
*/
|
|
142
|
+
setRequest(context: RequestContext): void
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ===== Request Context =====
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Contexto da request disponível para os guards.
|
|
149
|
+
* Abstrai o acesso a headers, cookies, etc.
|
|
150
|
+
*/
|
|
151
|
+
export interface RequestContext {
|
|
152
|
+
/** Headers da request */
|
|
153
|
+
headers: Record<string, string | undefined>
|
|
154
|
+
|
|
155
|
+
/** Cookies da request */
|
|
156
|
+
cookie: Record<string, { value: string; set: (opts: CookieOptions) => void } | undefined>
|
|
157
|
+
|
|
158
|
+
/** Setter para cookies na response */
|
|
159
|
+
setCookie: (name: string, value: string, options?: CookieOptions) => void
|
|
160
|
+
|
|
161
|
+
/** Remove um cookie */
|
|
162
|
+
removeCookie: (name: string) => void
|
|
163
|
+
|
|
164
|
+
/** IP da request (para rate limiting) */
|
|
165
|
+
ip: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Opções para cookies */
|
|
169
|
+
export interface CookieOptions {
|
|
170
|
+
maxAge?: number
|
|
171
|
+
httpOnly?: boolean
|
|
172
|
+
secure?: boolean
|
|
173
|
+
sameSite?: 'strict' | 'lax' | 'none'
|
|
174
|
+
path?: string
|
|
175
|
+
domain?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ===== Guard Factory =====
|
|
179
|
+
|
|
180
|
+
/** Config de um guard individual */
|
|
181
|
+
export interface GuardConfig {
|
|
182
|
+
driver: string
|
|
183
|
+
provider: string
|
|
184
|
+
[key: string]: unknown
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Factory function para criar guards customizados */
|
|
188
|
+
export type GuardFactory = (
|
|
189
|
+
name: string,
|
|
190
|
+
config: GuardConfig,
|
|
191
|
+
provider: UserProvider
|
|
192
|
+
) => Guard
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - Session Guard
|
|
3
|
+
*
|
|
4
|
+
* Guard baseado em session cookie. Padrão para SPAs que se comunicam
|
|
5
|
+
* com a API via fetch/axios (mesma origem ou CORS com credentials).
|
|
6
|
+
*
|
|
7
|
+
* Fluxo:
|
|
8
|
+
* 1. POST /login → attempt() → valida credenciais → cria sessão → seta cookie
|
|
9
|
+
* 2. Requests seguintes → cookie enviado automaticamente → user() resolve da sessão
|
|
10
|
+
* 3. POST /logout → logout() → destroi sessão → remove cookie
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
Guard,
|
|
15
|
+
Authenticatable,
|
|
16
|
+
UserProvider,
|
|
17
|
+
RequestContext,
|
|
18
|
+
} from '../contracts'
|
|
19
|
+
import type { SessionManager } from '../sessions/SessionManager'
|
|
20
|
+
|
|
21
|
+
export class SessionGuard implements Guard {
|
|
22
|
+
readonly name: string
|
|
23
|
+
private provider: UserProvider
|
|
24
|
+
private sessions: SessionManager
|
|
25
|
+
private request: RequestContext | null = null
|
|
26
|
+
|
|
27
|
+
/** Cache do usuário para a request atual (evita múltiplas queries) */
|
|
28
|
+
private resolvedUser: Authenticatable | null | undefined = undefined
|
|
29
|
+
|
|
30
|
+
constructor(name: string, provider: UserProvider, sessions: SessionManager) {
|
|
31
|
+
this.name = name
|
|
32
|
+
this.provider = provider
|
|
33
|
+
this.sessions = sessions
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setRequest(context: RequestContext): void {
|
|
37
|
+
this.request = context
|
|
38
|
+
// Reset cache para nova request
|
|
39
|
+
this.resolvedUser = undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async user(): Promise<Authenticatable | null> {
|
|
43
|
+
// Cache per-request
|
|
44
|
+
if (this.resolvedUser !== undefined) {
|
|
45
|
+
return this.resolvedUser
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.resolvedUser = null
|
|
49
|
+
|
|
50
|
+
if (!this.request) return null
|
|
51
|
+
|
|
52
|
+
// 1. Ler session ID do cookie
|
|
53
|
+
const sessionId = this.getSessionId()
|
|
54
|
+
if (!sessionId) return null
|
|
55
|
+
|
|
56
|
+
// 2. Buscar dados da sessão
|
|
57
|
+
const sessionData = await this.sessions.read(sessionId)
|
|
58
|
+
if (!sessionData?.userId) return null
|
|
59
|
+
|
|
60
|
+
// 3. Buscar usuário pelo ID
|
|
61
|
+
const user = await this.provider.retrieveById(sessionData.userId as string | number)
|
|
62
|
+
if (user) {
|
|
63
|
+
this.resolvedUser = user
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return this.resolvedUser
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async id(): Promise<string | number | null> {
|
|
70
|
+
const user = await this.user()
|
|
71
|
+
return user?.getAuthId() ?? null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async check(): Promise<boolean> {
|
|
75
|
+
return (await this.user()) !== null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async guest(): Promise<boolean> {
|
|
79
|
+
return (await this.user()) === null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async attempt(
|
|
83
|
+
credentials: Record<string, unknown>,
|
|
84
|
+
_remember?: boolean
|
|
85
|
+
): Promise<Authenticatable | null> {
|
|
86
|
+
// 1. Buscar usuário pelas credenciais (SEM password)
|
|
87
|
+
const user = await this.provider.retrieveByCredentials(credentials)
|
|
88
|
+
if (!user) return null
|
|
89
|
+
|
|
90
|
+
// 2. Validar password
|
|
91
|
+
const valid = await this.provider.validateCredentials(user, credentials)
|
|
92
|
+
if (!valid) return null
|
|
93
|
+
|
|
94
|
+
// 3. Login
|
|
95
|
+
await this.login(user)
|
|
96
|
+
|
|
97
|
+
return user
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async login(user: Authenticatable, _remember?: boolean): Promise<void> {
|
|
101
|
+
if (!this.request) {
|
|
102
|
+
throw new Error('SessionGuard: request context not set. Call setRequest() first.')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const config = this.sessions.getConfig()
|
|
106
|
+
|
|
107
|
+
// 1. Regenerar sessão (ou criar nova) para prevenir session fixation
|
|
108
|
+
const existingSessionId = this.getSessionId()
|
|
109
|
+
let sessionId: string
|
|
110
|
+
|
|
111
|
+
if (existingSessionId) {
|
|
112
|
+
sessionId = await this.sessions.regenerate(existingSessionId)
|
|
113
|
+
} else {
|
|
114
|
+
sessionId = await this.sessions.create({})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2. Salvar userId na sessão
|
|
118
|
+
await this.sessions.put(sessionId, 'userId', user.getAuthId())
|
|
119
|
+
await this.sessions.put(sessionId, '_loginAt', Date.now())
|
|
120
|
+
|
|
121
|
+
// 3. Setar cookie
|
|
122
|
+
this.request.setCookie(config.cookieName, sessionId, {
|
|
123
|
+
maxAge: config.lifetime,
|
|
124
|
+
httpOnly: config.httpOnly,
|
|
125
|
+
secure: config.secure,
|
|
126
|
+
sameSite: config.sameSite,
|
|
127
|
+
path: config.path,
|
|
128
|
+
domain: config.domain,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// 4. Cache do usuário para esta request
|
|
132
|
+
this.resolvedUser = user
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async logout(): Promise<void> {
|
|
136
|
+
if (!this.request) return
|
|
137
|
+
|
|
138
|
+
const config = this.sessions.getConfig()
|
|
139
|
+
const sessionId = this.getSessionId()
|
|
140
|
+
|
|
141
|
+
// 1. Destroir sessão
|
|
142
|
+
if (sessionId) {
|
|
143
|
+
await this.sessions.destroy(sessionId)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Remover cookie
|
|
147
|
+
this.request.removeCookie(config.cookieName)
|
|
148
|
+
|
|
149
|
+
// 3. Limpar cache
|
|
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
|
+
/** Lê o session ID do cookie */
|
|
160
|
+
private getSessionId(): string | null {
|
|
161
|
+
if (!this.request) return null
|
|
162
|
+
|
|
163
|
+
const config = this.sessions.getConfig()
|
|
164
|
+
const cookie = this.request.cookie[config.cookieName]
|
|
165
|
+
return cookie?.value || null
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -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
|
+
}
|