create-fluxstack 1.12.1 → 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/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/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
package/app/client/src/App.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { CounterDemo } from './live/CounterDemo'
|
|
|
7
7
|
import { UploadDemo } from './live/UploadDemo'
|
|
8
8
|
import { ChatDemo } from './live/ChatDemo'
|
|
9
9
|
import { RoomChatDemo } from './live/RoomChatDemo'
|
|
10
|
+
import { AuthDemo } from './live/AuthDemo'
|
|
10
11
|
import { AppLayout } from './components/AppLayout'
|
|
11
12
|
import { DemoPage } from './components/DemoPage'
|
|
12
13
|
import { HomePage } from './pages/HomePage'
|
|
@@ -126,6 +127,16 @@ function AppContent() {
|
|
|
126
127
|
</DemoPage>
|
|
127
128
|
}
|
|
128
129
|
/>
|
|
130
|
+
<Route
|
|
131
|
+
path="/auth"
|
|
132
|
+
element={
|
|
133
|
+
<DemoPage
|
|
134
|
+
note={<>🔒 Sistema de autenticação declarativo para Live Components com <code className="text-purple-400">$auth</code>!</>}
|
|
135
|
+
>
|
|
136
|
+
<AuthDemo />
|
|
137
|
+
</DemoPage>
|
|
138
|
+
}
|
|
139
|
+
/>
|
|
129
140
|
<Route path="*" element={<HomePage apiStatus={apiStatus} />} />
|
|
130
141
|
</Route>
|
|
131
142
|
</Routes>
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// 🔒 AuthDemo - Exemplo completo de autenticação em Live Components
|
|
2
|
+
//
|
|
3
|
+
// Demonstra:
|
|
4
|
+
// 1. Conexão autenticada via LiveComponentsProvider
|
|
5
|
+
// 2. Auth dinâmico via useLiveComponents().authenticate()
|
|
6
|
+
// 3. Componente público (sem auth)
|
|
7
|
+
// 4. Componente protegido (requer auth + role)
|
|
8
|
+
// 5. Actions protegidas por permissão
|
|
9
|
+
// 6. Leitura de $authenticated no proxy
|
|
10
|
+
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
import { Live, useLiveComponents } from '@/core/client'
|
|
13
|
+
import type { LiveAuthOptions } from '@/core/client'
|
|
14
|
+
import { LiveCounter } from '@server/live/LiveCounter'
|
|
15
|
+
import { LiveAdminPanel } from '@server/live/LiveAdminPanel'
|
|
16
|
+
|
|
17
|
+
// ───────────────────────────────────────
|
|
18
|
+
// 1. Componente público (sem auth)
|
|
19
|
+
// Funciona para qualquer visitante
|
|
20
|
+
// ───────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function PublicSection() {
|
|
23
|
+
const counter = Live.use(LiveCounter, {
|
|
24
|
+
room: 'public-counter',
|
|
25
|
+
initialState: LiveCounter.defaultState,
|
|
26
|
+
persistState: false,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
|
31
|
+
<h3 className="text-lg font-semibold text-white mb-1">Contador Público</h3>
|
|
32
|
+
<p className="text-gray-400 text-xs mb-4">Sem autenticação necessária</p>
|
|
33
|
+
|
|
34
|
+
<div className="flex items-center gap-4">
|
|
35
|
+
<button
|
|
36
|
+
onClick={() => counter.decrement()}
|
|
37
|
+
className="px-3 py-1 rounded bg-red-500/20 text-red-300 hover:bg-red-500/30"
|
|
38
|
+
>
|
|
39
|
+
−
|
|
40
|
+
</button>
|
|
41
|
+
<span className="text-3xl font-bold text-white">{counter.$state.count}</span>
|
|
42
|
+
<button
|
|
43
|
+
onClick={() => counter.increment()}
|
|
44
|
+
className="px-3 py-1 rounded bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30"
|
|
45
|
+
>
|
|
46
|
+
+
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div className="mt-3 text-xs text-gray-500">
|
|
51
|
+
$authenticated: <code className="text-yellow-300">{String(counter.$authenticated)}</code>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ───────────────────────────────────────
|
|
58
|
+
// 2. Painel admin (requer auth + role)
|
|
59
|
+
// Demonstra $auth no servidor
|
|
60
|
+
// ───────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function AdminSection() {
|
|
63
|
+
const [newUserName, setNewUserName] = useState('')
|
|
64
|
+
const [error, setError] = useState<string | null>(null)
|
|
65
|
+
|
|
66
|
+
const panel = Live.use(LiveAdminPanel, { persistState: false })
|
|
67
|
+
|
|
68
|
+
// Se não autenticado ou sem permissão, o mount falha com AUTH_DENIED
|
|
69
|
+
if (panel.$error?.includes('AUTH_DENIED')) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-6">
|
|
72
|
+
<h3 className="text-lg font-semibold text-red-300 mb-2">Painel Admin</h3>
|
|
73
|
+
<p className="text-red-400 text-sm">
|
|
74
|
+
Acesso negado: {panel.$error}
|
|
75
|
+
</p>
|
|
76
|
+
<p className="text-gray-400 text-xs mt-2">
|
|
77
|
+
Autentique-se com role "admin" para acessar.
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (panel.$status === 'mounting' || panel.$loading) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
|
86
|
+
<div className="flex items-center gap-2 text-gray-400">
|
|
87
|
+
<div className="w-4 h-4 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
|
|
88
|
+
Montando painel admin...
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handleAddUser = async () => {
|
|
95
|
+
if (!newUserName.trim()) return
|
|
96
|
+
try {
|
|
97
|
+
await panel.addUser({ name: newUserName.trim(), role: 'user' })
|
|
98
|
+
setNewUserName('')
|
|
99
|
+
setError(null)
|
|
100
|
+
} catch (e: any) {
|
|
101
|
+
setError(e.message)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const handleDeleteUser = async (userId: string) => {
|
|
106
|
+
try {
|
|
107
|
+
await panel.deleteUser({ userId })
|
|
108
|
+
setError(null)
|
|
109
|
+
} catch (e: any) {
|
|
110
|
+
// Se AUTH_DENIED, significa que faltou a permissão 'users.delete'
|
|
111
|
+
setError(e.message)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
|
117
|
+
<div className="flex items-center justify-between mb-4">
|
|
118
|
+
<div>
|
|
119
|
+
<h3 className="text-lg font-semibold text-white">Painel Admin</h3>
|
|
120
|
+
<p className="text-gray-400 text-xs">
|
|
121
|
+
Requer: <code className="text-purple-300">auth.required + roles: ['admin']</code>
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="text-xs text-right">
|
|
125
|
+
<div className="text-gray-400">
|
|
126
|
+
User: <span className="text-emerald-300">{panel.$state.currentUser || '...'}</span>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="text-gray-400">
|
|
129
|
+
Roles: <span className="text-yellow-300">{panel.$state.currentRoles.join(', ') || '...'}</span>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{error && (
|
|
135
|
+
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 mb-4 text-red-300 text-sm">
|
|
136
|
+
{error}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{/* User list */}
|
|
141
|
+
<div className="space-y-2 mb-4">
|
|
142
|
+
{panel.$state.users.map(user => (
|
|
143
|
+
<div key={user.id} className="flex items-center justify-between bg-black/20 rounded-lg px-4 py-2">
|
|
144
|
+
<div>
|
|
145
|
+
<span className="text-white font-medium">{user.name}</span>
|
|
146
|
+
<span className="text-gray-500 text-xs ml-2">({user.role})</span>
|
|
147
|
+
</div>
|
|
148
|
+
<button
|
|
149
|
+
onClick={() => handleDeleteUser(user.id)}
|
|
150
|
+
className="text-xs px-2 py-1 rounded bg-red-500/20 text-red-300 hover:bg-red-500/30"
|
|
151
|
+
title="Requer permissão 'users.delete'"
|
|
152
|
+
>
|
|
153
|
+
Deletar
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Add user */}
|
|
160
|
+
<div className="flex gap-2">
|
|
161
|
+
<input
|
|
162
|
+
value={newUserName}
|
|
163
|
+
onChange={e => setNewUserName(e.target.value)}
|
|
164
|
+
onKeyDown={e => e.key === 'Enter' && handleAddUser()}
|
|
165
|
+
placeholder="Nome do usuário..."
|
|
166
|
+
className="flex-1 px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white text-sm placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50"
|
|
167
|
+
/>
|
|
168
|
+
<button
|
|
169
|
+
onClick={handleAddUser}
|
|
170
|
+
className="px-4 py-2 rounded-lg bg-purple-500/20 border border-purple-500/30 text-purple-200 text-sm hover:bg-purple-500/30"
|
|
171
|
+
>
|
|
172
|
+
Adicionar
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Audit log */}
|
|
177
|
+
{panel.$state.audit.length > 0 && (
|
|
178
|
+
<div className="mt-4 pt-4 border-t border-white/10">
|
|
179
|
+
<div className="flex items-center justify-between mb-2">
|
|
180
|
+
<h4 className="text-sm font-semibold text-gray-300">Audit Log</h4>
|
|
181
|
+
<button
|
|
182
|
+
onClick={() => panel.clearAudit()}
|
|
183
|
+
className="text-xs px-2 py-1 rounded bg-gray-500/20 text-gray-400 hover:bg-gray-500/30"
|
|
184
|
+
title="Requer role 'admin'"
|
|
185
|
+
>
|
|
186
|
+
Limpar
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="space-y-1 max-h-32 overflow-auto">
|
|
190
|
+
{panel.$state.audit.map((entry, i) => (
|
|
191
|
+
<div key={i} className="text-xs text-gray-500">
|
|
192
|
+
<span className="text-gray-400">{new Date(entry.timestamp).toLocaleTimeString()}</span>
|
|
193
|
+
{' '}<span className="text-blue-300">{entry.action}</span>
|
|
194
|
+
{' '}by <span className="text-emerald-300">{entry.performedBy}</span>
|
|
195
|
+
{entry.target && <> on <span className="text-yellow-300">{entry.target}</span></>}
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ───────────────────────────────────────
|
|
206
|
+
// 3. Controle de autenticação
|
|
207
|
+
// Simula login/logout via authenticate()
|
|
208
|
+
// ───────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
function AuthControls() {
|
|
211
|
+
const { authenticated, authenticate, reconnect } = useLiveComponents()
|
|
212
|
+
const [token, setToken] = useState('')
|
|
213
|
+
const [isLoggingIn, setIsLoggingIn] = useState(false)
|
|
214
|
+
|
|
215
|
+
const handleLogin = async () => {
|
|
216
|
+
if (!token.trim()) return
|
|
217
|
+
setIsLoggingIn(true)
|
|
218
|
+
await authenticate({ token: token.trim() })
|
|
219
|
+
setIsLoggingIn(false)
|
|
220
|
+
// Componentes detectam automaticamente a mudança de auth e remontam
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const handleLogout = () => {
|
|
224
|
+
setToken('')
|
|
225
|
+
// Reconectar sem token = nova conexão anônima
|
|
226
|
+
reconnect()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div className="bg-white/5 border border-white/10 rounded-xl p-6 mb-6">
|
|
231
|
+
<h3 className="text-lg font-semibold text-white mb-2">Autenticação</h3>
|
|
232
|
+
|
|
233
|
+
<div className="flex items-center gap-3 mb-4">
|
|
234
|
+
<div className={`w-3 h-3 rounded-full ${authenticated ? 'bg-emerald-400' : 'bg-gray-500'}`} />
|
|
235
|
+
<span className={`text-sm ${authenticated ? 'text-emerald-300' : 'text-gray-400'}`}>
|
|
236
|
+
{authenticated ? 'Autenticado' : 'Não autenticado'}
|
|
237
|
+
</span>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div className="flex gap-2">
|
|
241
|
+
<input
|
|
242
|
+
value={token}
|
|
243
|
+
onChange={e => setToken(e.target.value)}
|
|
244
|
+
onKeyDown={e => e.key === 'Enter' && handleLogin()}
|
|
245
|
+
placeholder="Token (JWT, API key, etc.)"
|
|
246
|
+
className="flex-1 px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white text-sm placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50"
|
|
247
|
+
/>
|
|
248
|
+
<button
|
|
249
|
+
onClick={handleLogin}
|
|
250
|
+
disabled={isLoggingIn}
|
|
251
|
+
className="px-4 py-2 rounded-lg bg-emerald-500/20 border border-emerald-500/30 text-emerald-200 text-sm hover:bg-emerald-500/30 disabled:opacity-50"
|
|
252
|
+
>
|
|
253
|
+
{isLoggingIn ? 'Autenticando...' : 'Login'}
|
|
254
|
+
</button>
|
|
255
|
+
{authenticated && (
|
|
256
|
+
<button
|
|
257
|
+
onClick={handleLogout}
|
|
258
|
+
className="px-4 py-2 rounded-lg bg-red-500/20 border border-red-500/30 text-red-200 text-sm hover:bg-red-500/30"
|
|
259
|
+
>
|
|
260
|
+
Logout
|
|
261
|
+
</button>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div className="mt-4 p-3 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
|
|
266
|
+
<p className="text-emerald-300 text-xs font-semibold mb-2">Tokens de teste (dev only):</p>
|
|
267
|
+
<div className="grid grid-cols-3 gap-2 text-xs">
|
|
268
|
+
<button
|
|
269
|
+
onClick={() => { setToken('admin-token'); }}
|
|
270
|
+
className="px-2 py-1 rounded bg-purple-500/20 text-purple-300 hover:bg-purple-500/30"
|
|
271
|
+
>
|
|
272
|
+
admin-token
|
|
273
|
+
</button>
|
|
274
|
+
<button
|
|
275
|
+
onClick={() => { setToken('user-token'); }}
|
|
276
|
+
className="px-2 py-1 rounded bg-blue-500/20 text-blue-300 hover:bg-blue-500/30"
|
|
277
|
+
>
|
|
278
|
+
user-token
|
|
279
|
+
</button>
|
|
280
|
+
<button
|
|
281
|
+
onClick={() => { setToken('mod-token'); }}
|
|
282
|
+
className="px-2 py-1 rounded bg-yellow-500/20 text-yellow-300 hover:bg-yellow-500/30"
|
|
283
|
+
>
|
|
284
|
+
mod-token
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
<p className="text-gray-500 text-xs mt-2">
|
|
288
|
+
Clique para preencher o campo, depois clique em Login.
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<p className="text-gray-500 text-xs mt-3">
|
|
293
|
+
Fluxo: <code>authenticate({ token })</code> envia mensagem <code>AUTH</code> via WebSocket.
|
|
294
|
+
O servidor valida via <code>LiveAuthProvider</code> registrado.
|
|
295
|
+
</p>
|
|
296
|
+
</div>
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ───────────────────────────────────────
|
|
301
|
+
// 4. Demo principal
|
|
302
|
+
// ───────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
export function AuthDemo() {
|
|
305
|
+
return (
|
|
306
|
+
<div className="space-y-6 w-full max-w-2xl mx-auto">
|
|
307
|
+
<div className="text-center mb-8">
|
|
308
|
+
<h2 className="text-3xl font-bold text-white mb-2">Live Components Auth</h2>
|
|
309
|
+
<p className="text-gray-400">
|
|
310
|
+
Sistema de autenticação declarativo para componentes real-time
|
|
311
|
+
</p>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<AuthControls />
|
|
315
|
+
|
|
316
|
+
<div className="grid gap-6">
|
|
317
|
+
<PublicSection />
|
|
318
|
+
<AdminSection />
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div className="bg-white/5 border border-white/10 rounded-xl p-6 text-xs text-gray-500 space-y-2">
|
|
322
|
+
<h4 className="text-sm font-semibold text-gray-300 mb-3">Como funciona</h4>
|
|
323
|
+
<p><strong className="text-purple-300">Server:</strong> <code>static auth = { required: true, roles: ['admin'] }</code></p>
|
|
324
|
+
<p><strong className="text-purple-300">Server:</strong> <code>static actionAuth = { deleteUser: { permissions: ['users.delete'] } }</code></p>
|
|
325
|
+
<p><strong className="text-purple-300">Server:</strong> <code>this.$auth.hasRole('admin')</code> dentro das actions</p>
|
|
326
|
+
<p><strong className="text-blue-300">Client:</strong> <code>component.$authenticated</code> no proxy</p>
|
|
327
|
+
<p><strong className="text-blue-300">Client:</strong> <code>useLiveComponents().authenticate({ token })</code> para login</p>
|
|
328
|
+
<p><strong className="text-blue-300">Client:</strong> <code><LiveComponentsProvider auth={{ token }}></code> para auth na conexão</p>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
@@ -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
|
+
}
|