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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - Auth Manager
|
|
3
|
+
*
|
|
4
|
+
* Orquestrador central do sistema de autenticação.
|
|
5
|
+
* Factory pattern com lazy resolution e cache de guards.
|
|
6
|
+
*
|
|
7
|
+
* Inspirado no AuthManager do Laravel:
|
|
8
|
+
* - Resolve guards por nome (ou default)
|
|
9
|
+
* - Extensível com extend() para guards customizados
|
|
10
|
+
* - Resolve providers automaticamente da config
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // Usar guard padrão
|
|
14
|
+
* const user = await auth.guard().user()
|
|
15
|
+
*
|
|
16
|
+
* // Usar guard específico
|
|
17
|
+
* const apiUser = await auth.guard('api').user()
|
|
18
|
+
*
|
|
19
|
+
* // Registrar guard customizado
|
|
20
|
+
* auth.extend('jwt', (name, config, provider) => new JWTGuard(...))
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
Guard,
|
|
26
|
+
UserProvider,
|
|
27
|
+
GuardConfig,
|
|
28
|
+
GuardFactory,
|
|
29
|
+
RequestContext,
|
|
30
|
+
} from './contracts'
|
|
31
|
+
import { SessionGuard } from './guards/SessionGuard'
|
|
32
|
+
import { TokenGuard } from './guards/TokenGuard'
|
|
33
|
+
import { SessionManager } from './sessions/SessionManager'
|
|
34
|
+
import { cacheManager } from '@server/cache'
|
|
35
|
+
|
|
36
|
+
export interface AuthManagerConfig {
|
|
37
|
+
defaults: {
|
|
38
|
+
guard: string
|
|
39
|
+
provider: string
|
|
40
|
+
}
|
|
41
|
+
guards: Record<string, GuardConfig>
|
|
42
|
+
providers: Record<string, ProviderConfig>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ProviderConfig {
|
|
46
|
+
driver: string
|
|
47
|
+
[key: string]: unknown
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class AuthManager {
|
|
51
|
+
private config: AuthManagerConfig
|
|
52
|
+
private guards = new Map<string, Guard>()
|
|
53
|
+
private customGuardFactories = new Map<string, GuardFactory>()
|
|
54
|
+
private providerInstances = new Map<string, UserProvider>()
|
|
55
|
+
private customProviderFactories = new Map<string, (config: ProviderConfig) => UserProvider>()
|
|
56
|
+
private sessionManager: SessionManager
|
|
57
|
+
|
|
58
|
+
constructor(config: AuthManagerConfig, sessionManager: SessionManager) {
|
|
59
|
+
this.config = config
|
|
60
|
+
this.sessionManager = sessionManager
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Retorna um guard por nome (ou o default).
|
|
65
|
+
* Guards são criados uma vez e reutilizados.
|
|
66
|
+
*/
|
|
67
|
+
guard(name?: string): Guard {
|
|
68
|
+
const guardName = name ?? this.config.defaults.guard
|
|
69
|
+
|
|
70
|
+
if (this.guards.has(guardName)) {
|
|
71
|
+
return this.guards.get(guardName)!
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const guard = this.resolve(guardName)
|
|
75
|
+
this.guards.set(guardName, guard)
|
|
76
|
+
return guard
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Inicializa todos os guards com o contexto da request.
|
|
81
|
+
* Deve ser chamado no middleware, antes de qualquer uso.
|
|
82
|
+
*/
|
|
83
|
+
setRequest(context: RequestContext): void {
|
|
84
|
+
for (const guard of this.guards.values()) {
|
|
85
|
+
guard.setRequest(context)
|
|
86
|
+
}
|
|
87
|
+
// Também resetar guards não-instanciados para forçar re-resolve com novo context
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Cria um guard novo (sem cache) com o context da request.
|
|
92
|
+
* Útil quando precisa de um guard fresco para cada request.
|
|
93
|
+
*/
|
|
94
|
+
freshGuard(name?: string, context?: RequestContext): Guard {
|
|
95
|
+
const guardName = name ?? this.config.defaults.guard
|
|
96
|
+
const guard = this.resolve(guardName)
|
|
97
|
+
if (context) {
|
|
98
|
+
guard.setRequest(context)
|
|
99
|
+
}
|
|
100
|
+
return guard
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Registra um guard driver customizado.
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* auth.extend('jwt', (name, config, provider) => {
|
|
108
|
+
* return new JWTGuard(name, provider, config.secret)
|
|
109
|
+
* })
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
extend(driver: string, factory: GuardFactory): void {
|
|
113
|
+
this.customGuardFactories.set(driver, factory)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Registra um provider customizado.
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* auth.extendProvider('drizzle', (config) => {
|
|
121
|
+
* return new DrizzleUserProvider(db, config.table)
|
|
122
|
+
* })
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
extendProvider(driver: string, factory: (config: ProviderConfig) => UserProvider): void {
|
|
126
|
+
this.customProviderFactories.set(driver, factory)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Registra uma instância de provider diretamente.
|
|
131
|
+
*/
|
|
132
|
+
registerProvider(name: string, provider: UserProvider): void {
|
|
133
|
+
this.providerInstances.set(name, provider)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Retorna o nome do guard padrão */
|
|
137
|
+
getDefaultGuardName(): string {
|
|
138
|
+
return this.config.defaults.guard
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Retorna a config */
|
|
142
|
+
getConfig(): AuthManagerConfig {
|
|
143
|
+
return this.config
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Resolve um guard por nome */
|
|
147
|
+
private resolve(name: string): Guard {
|
|
148
|
+
const guardConfig = this.config.guards[name]
|
|
149
|
+
if (!guardConfig) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Auth guard '${name}' not configured. ` +
|
|
152
|
+
`Available: ${Object.keys(this.config.guards).join(', ')}`
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Resolver provider
|
|
157
|
+
const provider = this.resolveProvider(guardConfig.provider)
|
|
158
|
+
|
|
159
|
+
// Verificar custom factory primeiro
|
|
160
|
+
if (this.customGuardFactories.has(guardConfig.driver)) {
|
|
161
|
+
return this.customGuardFactories.get(guardConfig.driver)!(name, guardConfig, provider)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Built-in drivers
|
|
165
|
+
switch (guardConfig.driver) {
|
|
166
|
+
case 'session':
|
|
167
|
+
return new SessionGuard(name, provider, this.sessionManager)
|
|
168
|
+
|
|
169
|
+
case 'token':
|
|
170
|
+
return new TokenGuard(
|
|
171
|
+
name,
|
|
172
|
+
provider,
|
|
173
|
+
cacheManager.driver(),
|
|
174
|
+
guardConfig.tokenTtl as number | undefined
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
default:
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Auth guard driver '${guardConfig.driver}' not supported. ` +
|
|
180
|
+
`Use auth.extend('${guardConfig.driver}', factory) to register it.`
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Resolve um provider por nome */
|
|
186
|
+
private resolveProvider(name: string): UserProvider {
|
|
187
|
+
// Cache de instâncias
|
|
188
|
+
if (this.providerInstances.has(name)) {
|
|
189
|
+
return this.providerInstances.get(name)!
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const providerConfig = this.config.providers[name]
|
|
193
|
+
if (!providerConfig) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Auth provider '${name}' not configured. ` +
|
|
196
|
+
`Available: ${Object.keys(this.config.providers).join(', ')}`
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Custom factory
|
|
201
|
+
if (this.customProviderFactories.has(providerConfig.driver)) {
|
|
202
|
+
const provider = this.customProviderFactories.get(providerConfig.driver)!(providerConfig)
|
|
203
|
+
this.providerInstances.set(name, provider)
|
|
204
|
+
return provider
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Auth provider driver '${providerConfig.driver}' not supported. ` +
|
|
209
|
+
`Use auth.extendProvider('${providerConfig.driver}', factory) to register it, ` +
|
|
210
|
+
`or use auth.registerProvider('${name}', providerInstance) to register directly.`
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// 🧪 DevAuthProvider - Provider de desenvolvimento para testes de auth
|
|
2
|
+
//
|
|
3
|
+
// Aceita tokens simples para facilitar testes da demo de autenticação.
|
|
4
|
+
// NÃO USAR EM PRODUÇÃO!
|
|
5
|
+
//
|
|
6
|
+
// Tokens válidos:
|
|
7
|
+
// - "admin-token" → role: admin, permissions: all
|
|
8
|
+
// - "user-token" → role: user, permissions: básicas
|
|
9
|
+
// - "mod-token" → role: moderator, permissions: moderação
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
LiveAuthProvider,
|
|
13
|
+
LiveAuthCredentials,
|
|
14
|
+
LiveAuthContext,
|
|
15
|
+
} from '@core/server/live/auth/types'
|
|
16
|
+
import { AuthenticatedContext } from '@core/server/live/auth/LiveAuthContext'
|
|
17
|
+
|
|
18
|
+
interface DevUser {
|
|
19
|
+
id: string
|
|
20
|
+
name: string
|
|
21
|
+
roles: string[]
|
|
22
|
+
permissions: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEV_USERS: Record<string, DevUser> = {
|
|
26
|
+
'admin-token': {
|
|
27
|
+
id: 'admin-1',
|
|
28
|
+
name: 'Admin User',
|
|
29
|
+
roles: ['admin', 'user'],
|
|
30
|
+
permissions: ['users.read', 'users.write', 'users.delete', 'chat.read', 'chat.write', 'chat.admin'],
|
|
31
|
+
},
|
|
32
|
+
'user-token': {
|
|
33
|
+
id: 'user-1',
|
|
34
|
+
name: 'Regular User',
|
|
35
|
+
roles: ['user'],
|
|
36
|
+
permissions: ['chat.read', 'chat.write'],
|
|
37
|
+
},
|
|
38
|
+
'mod-token': {
|
|
39
|
+
id: 'mod-1',
|
|
40
|
+
name: 'Moderator',
|
|
41
|
+
roles: ['moderator', 'user'],
|
|
42
|
+
permissions: ['chat.read', 'chat.write', 'chat.moderate'],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class DevAuthProvider implements LiveAuthProvider {
|
|
47
|
+
readonly name = 'dev'
|
|
48
|
+
|
|
49
|
+
async authenticate(credentials: LiveAuthCredentials): Promise<LiveAuthContext | null> {
|
|
50
|
+
const token = credentials.token as string
|
|
51
|
+
if (!token) return null
|
|
52
|
+
|
|
53
|
+
const user = DEV_USERS[token]
|
|
54
|
+
if (!user) return null
|
|
55
|
+
|
|
56
|
+
return new AuthenticatedContext(
|
|
57
|
+
{
|
|
58
|
+
id: user.id,
|
|
59
|
+
name: user.name,
|
|
60
|
+
roles: user.roles,
|
|
61
|
+
permissions: user.permissions,
|
|
62
|
+
},
|
|
63
|
+
token
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - Hash Manager
|
|
3
|
+
*
|
|
4
|
+
* Abstração de password hashing usando APIs nativas do Bun.
|
|
5
|
+
* Suporta bcrypt e argon2id. Inclui needsRehash() para
|
|
6
|
+
* migração transparente de algoritmos.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const hash = await Hash.make('my-password')
|
|
10
|
+
* const valid = await Hash.check('my-password', hash)
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type HashAlgorithm = 'bcrypt' | 'argon2id'
|
|
15
|
+
|
|
16
|
+
export interface HashOptions {
|
|
17
|
+
algorithm?: HashAlgorithm
|
|
18
|
+
/** Cost/rounds para bcrypt (default: 10) */
|
|
19
|
+
bcryptRounds?: number
|
|
20
|
+
/** Memory cost para argon2 em KiB (default: 65536) */
|
|
21
|
+
argon2MemoryCost?: number
|
|
22
|
+
/** Time cost para argon2 (default: 2) */
|
|
23
|
+
argon2TimeCost?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class HashManager {
|
|
27
|
+
private algorithm: HashAlgorithm
|
|
28
|
+
private bcryptRounds: number
|
|
29
|
+
private argon2MemoryCost: number
|
|
30
|
+
private argon2TimeCost: number
|
|
31
|
+
|
|
32
|
+
constructor(options: HashOptions = {}) {
|
|
33
|
+
this.algorithm = options.algorithm ?? 'bcrypt'
|
|
34
|
+
this.bcryptRounds = options.bcryptRounds ?? 10
|
|
35
|
+
this.argon2MemoryCost = options.argon2MemoryCost ?? 65536
|
|
36
|
+
this.argon2TimeCost = options.argon2TimeCost ?? 2
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cria hash de uma password.
|
|
41
|
+
*/
|
|
42
|
+
async make(plaintext: string): Promise<string> {
|
|
43
|
+
if (this.algorithm === 'argon2id') {
|
|
44
|
+
return Bun.password.hash(plaintext, {
|
|
45
|
+
algorithm: 'argon2id',
|
|
46
|
+
memoryCost: this.argon2MemoryCost,
|
|
47
|
+
timeCost: this.argon2TimeCost,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return Bun.password.hash(plaintext, {
|
|
52
|
+
algorithm: 'bcrypt',
|
|
53
|
+
cost: this.bcryptRounds,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Verifica se plaintext corresponde ao hash.
|
|
59
|
+
*/
|
|
60
|
+
async check(plaintext: string, hash: string): Promise<boolean> {
|
|
61
|
+
return Bun.password.verify(plaintext, hash)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Verifica se o hash precisa ser re-gerado.
|
|
66
|
+
* Útil para migrar de bcrypt → argon2 ou mudar rounds.
|
|
67
|
+
*
|
|
68
|
+
* Uso típico: após login bem-sucedido, verificar e re-hash se necessário.
|
|
69
|
+
*/
|
|
70
|
+
needsRehash(hash: string): boolean {
|
|
71
|
+
if (this.algorithm === 'bcrypt') {
|
|
72
|
+
// bcrypt hashes começam com $2b$ ou $2a$
|
|
73
|
+
if (!hash.startsWith('$2b$') && !hash.startsWith('$2a$')) {
|
|
74
|
+
return true // Hash não é bcrypt, precisa migrar
|
|
75
|
+
}
|
|
76
|
+
// Verificar rounds: $2b$10$ → rounds = 10
|
|
77
|
+
const roundsMatch = hash.match(/^\$2[ab]\$(\d+)\$/)
|
|
78
|
+
if (roundsMatch) {
|
|
79
|
+
const currentRounds = parseInt(roundsMatch[1], 10)
|
|
80
|
+
return currentRounds !== this.bcryptRounds
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.algorithm === 'argon2id') {
|
|
86
|
+
return !hash.startsWith('$argon2id$')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Retorna o algoritmo atual */
|
|
93
|
+
getAlgorithm(): HashAlgorithm {
|
|
94
|
+
return this.algorithm
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Instância global do hash manager (configurada no boot) */
|
|
99
|
+
let hashInstance: HashManager | null = null
|
|
100
|
+
|
|
101
|
+
export function getHashManager(): HashManager {
|
|
102
|
+
if (!hashInstance) {
|
|
103
|
+
hashInstance = new HashManager()
|
|
104
|
+
}
|
|
105
|
+
return hashInstance
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function setHashManager(manager: HashManager): void {
|
|
109
|
+
hashInstance = manager
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Atalho para uso direto */
|
|
113
|
+
export const Hash = {
|
|
114
|
+
async make(plaintext: string): Promise<string> {
|
|
115
|
+
return getHashManager().make(plaintext)
|
|
116
|
+
},
|
|
117
|
+
async check(plaintext: string, hash: string): Promise<boolean> {
|
|
118
|
+
return getHashManager().check(plaintext, hash)
|
|
119
|
+
},
|
|
120
|
+
needsRehash(hash: string): boolean {
|
|
121
|
+
return getHashManager().needsRehash(hash)
|
|
122
|
+
},
|
|
123
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// 🔒 Exemplo: Como criar um LiveAuthProvider customizado (JWT)
|
|
2
|
+
//
|
|
3
|
+
// Este arquivo mostra como criar um provider de autenticação para Live Components.
|
|
4
|
+
// Copie e adapte para o seu caso de uso.
|
|
5
|
+
//
|
|
6
|
+
// Registro:
|
|
7
|
+
// import { liveAuthManager } from '@core/server/live/auth'
|
|
8
|
+
// import { JWTAuthProvider } from './auth/JWTAuthProvider'
|
|
9
|
+
//
|
|
10
|
+
// liveAuthManager.register(new JWTAuthProvider('your-secret-key'))
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
LiveAuthProvider,
|
|
14
|
+
LiveAuthCredentials,
|
|
15
|
+
LiveAuthContext,
|
|
16
|
+
} from '@core/server/live/auth/types'
|
|
17
|
+
import { AuthenticatedContext } from '@core/server/live/auth/LiveAuthContext'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Exemplo de provider JWT para Live Components.
|
|
21
|
+
*
|
|
22
|
+
* Em produção, use uma lib real como 'jose' ou 'jsonwebtoken'.
|
|
23
|
+
* Este exemplo usa decode simples para fins didáticos.
|
|
24
|
+
*/
|
|
25
|
+
export class JWTAuthProvider implements LiveAuthProvider {
|
|
26
|
+
readonly name = 'jwt'
|
|
27
|
+
private secret: string
|
|
28
|
+
|
|
29
|
+
constructor(secret: string) {
|
|
30
|
+
this.secret = secret
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async authenticate(credentials: LiveAuthCredentials): Promise<LiveAuthContext | null> {
|
|
34
|
+
const token = credentials.token as string
|
|
35
|
+
if (!token) return null
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Em produção: const payload = jwt.verify(token, this.secret)
|
|
39
|
+
const payload = this.decodeToken(token)
|
|
40
|
+
if (!payload) return null
|
|
41
|
+
|
|
42
|
+
return new AuthenticatedContext(
|
|
43
|
+
{
|
|
44
|
+
id: payload.sub,
|
|
45
|
+
roles: payload.roles || [],
|
|
46
|
+
permissions: payload.permissions || [],
|
|
47
|
+
name: payload.name,
|
|
48
|
+
email: payload.email,
|
|
49
|
+
},
|
|
50
|
+
token
|
|
51
|
+
)
|
|
52
|
+
} catch {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* (Opcional) Autorização customizada por action.
|
|
59
|
+
* Se implementado, é chamado ALÉM da verificação de roles/permissions.
|
|
60
|
+
* Útil para lógica de negócio complexa (ex: limites por plano, rate limiting).
|
|
61
|
+
*/
|
|
62
|
+
async authorizeAction(
|
|
63
|
+
context: LiveAuthContext,
|
|
64
|
+
componentName: string,
|
|
65
|
+
action: string
|
|
66
|
+
): Promise<boolean> {
|
|
67
|
+
// Exemplo: bloquear ações destrutivas fora do horário comercial
|
|
68
|
+
// const hour = new Date().getHours()
|
|
69
|
+
// if (action === 'deleteAll' && (hour < 9 || hour > 18)) return false
|
|
70
|
+
|
|
71
|
+
return true // Allow by default
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* (Opcional) Autorização customizada por sala.
|
|
76
|
+
* Útil para salas privadas, premium, etc.
|
|
77
|
+
*/
|
|
78
|
+
async authorizeRoom(
|
|
79
|
+
context: LiveAuthContext,
|
|
80
|
+
roomId: string
|
|
81
|
+
): Promise<boolean> {
|
|
82
|
+
// Exemplo: salas "vip-*" requerem role premium
|
|
83
|
+
// if (roomId.startsWith('vip-') && !context.hasRole('premium')) return false
|
|
84
|
+
|
|
85
|
+
return true // Allow by default
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Decode simplificado (NÃO USAR EM PRODUÇÃO - não valida assinatura)
|
|
89
|
+
private decodeToken(token: string): any {
|
|
90
|
+
try {
|
|
91
|
+
const parts = token.split('.')
|
|
92
|
+
if (parts.length !== 3) return null
|
|
93
|
+
const payload = JSON.parse(atob(parts[1]))
|
|
94
|
+
// Em produção: verificar expiração, assinatura, etc.
|
|
95
|
+
if (payload.exp && payload.exp * 1000 < Date.now()) return null
|
|
96
|
+
return payload
|
|
97
|
+
} catch {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Proteção contra brute-force. Usa o cache system modular.
|
|
5
|
+
* Inspirado no RateLimiter do Laravel (por chave: email+ip).
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const key = `login:${email}|${ip}`
|
|
9
|
+
* if (await rateLimiter.tooManyAttempts(key, 5)) {
|
|
10
|
+
* const seconds = await rateLimiter.availableIn(key)
|
|
11
|
+
* return { error: `Too many attempts. Try again in ${seconds}s` }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { CacheDriver } from '@server/cache/contracts'
|
|
17
|
+
|
|
18
|
+
interface RateLimitEntry {
|
|
19
|
+
count: number
|
|
20
|
+
expiresAt: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class RateLimiter {
|
|
24
|
+
private cache: CacheDriver
|
|
25
|
+
|
|
26
|
+
constructor(cache: CacheDriver) {
|
|
27
|
+
this.cache = cache
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Registra uma tentativa para a chave.
|
|
32
|
+
* @param key Chave identificadora (ex: 'login:user@email.com|127.0.0.1')
|
|
33
|
+
* @param decaySeconds Tempo em segundos até o contador resetar
|
|
34
|
+
*/
|
|
35
|
+
async hit(key: string, decaySeconds: number = 60): Promise<number> {
|
|
36
|
+
const cacheKey = `rate_limit:${key}`
|
|
37
|
+
const entry = await this.cache.get<RateLimitEntry>(cacheKey)
|
|
38
|
+
const now = Date.now()
|
|
39
|
+
|
|
40
|
+
if (entry && entry.expiresAt > now) {
|
|
41
|
+
// Incrementar contador existente
|
|
42
|
+
const updated: RateLimitEntry = {
|
|
43
|
+
count: entry.count + 1,
|
|
44
|
+
expiresAt: entry.expiresAt,
|
|
45
|
+
}
|
|
46
|
+
const remainingTtl = Math.ceil((entry.expiresAt - now) / 1000)
|
|
47
|
+
await this.cache.set(cacheKey, updated, remainingTtl)
|
|
48
|
+
return updated.count
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Nova entrada
|
|
52
|
+
const newEntry: RateLimitEntry = {
|
|
53
|
+
count: 1,
|
|
54
|
+
expiresAt: now + (decaySeconds * 1000),
|
|
55
|
+
}
|
|
56
|
+
await this.cache.set(cacheKey, newEntry, decaySeconds)
|
|
57
|
+
return 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Verifica se a chave excedeu o limite de tentativas.
|
|
62
|
+
*/
|
|
63
|
+
async tooManyAttempts(key: string, maxAttempts: number): Promise<boolean> {
|
|
64
|
+
const attempts = await this.attempts(key)
|
|
65
|
+
return attempts >= maxAttempts
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retorna o número atual de tentativas para a chave.
|
|
70
|
+
*/
|
|
71
|
+
async attempts(key: string): Promise<number> {
|
|
72
|
+
const cacheKey = `rate_limit:${key}`
|
|
73
|
+
const entry = await this.cache.get<RateLimitEntry>(cacheKey)
|
|
74
|
+
|
|
75
|
+
if (!entry) return 0
|
|
76
|
+
if (entry.expiresAt < Date.now()) return 0
|
|
77
|
+
|
|
78
|
+
return entry.count
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Retorna quantos segundos até o rate limit expirar.
|
|
83
|
+
*/
|
|
84
|
+
async availableIn(key: string): Promise<number> {
|
|
85
|
+
const cacheKey = `rate_limit:${key}`
|
|
86
|
+
const entry = await this.cache.get<RateLimitEntry>(cacheKey)
|
|
87
|
+
|
|
88
|
+
if (!entry) return 0
|
|
89
|
+
return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1000))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Retorna quantas tentativas restam.
|
|
94
|
+
*/
|
|
95
|
+
async remainingAttempts(key: string, maxAttempts: number): Promise<number> {
|
|
96
|
+
const attempts = await this.attempts(key)
|
|
97
|
+
return Math.max(0, maxAttempts - attempts)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Limpa o rate limit para a chave (após login bem-sucedido).
|
|
102
|
+
*/
|
|
103
|
+
async clear(key: string): Promise<void> {
|
|
104
|
+
await this.cache.delete(`rate_limit:${key}`)
|
|
105
|
+
}
|
|
106
|
+
}
|