create-fluxstack 1.17.0 → 1.18.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/resources/live-auth.md +462 -465
- package/app/client/.live-stubs/LiveAdminPanel.js +15 -0
- package/app/client/.live-stubs/LiveCounter.js +9 -0
- package/app/client/.live-stubs/LiveForm.js +11 -0
- package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +11 -0
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +45 -3
- package/app/client/src/components/AppLayout.tsx +10 -1
- package/app/client/src/components/ErrorBoundary.tsx +117 -0
- package/app/client/src/components/LiveErrorBoundary.tsx +87 -0
- package/app/client/src/components/LiveUploadWidget.tsx +10 -14
- package/app/client/src/lib/eden-api.ts +6 -0
- package/app/client/src/lib/plugin-hooks.ts +82 -0
- package/app/client/src/live/AuthDemo.tsx +0 -1
- package/app/client/src/live/FormDemo.tsx +1 -1
- package/app/client/src/live/PingPongDemo.tsx +4 -1
- package/app/client/src/live/RoomChatDemo.tsx +90 -50
- package/app/client/src/live/SharedCounterDemo.tsx +5 -0
- package/app/server/auth/AuthManager.ts +24 -0
- package/app/server/auth/contracts.ts +12 -1
- package/app/server/auth/errors.ts +84 -0
- package/app/server/auth/guards/TokenGuard.ts +5 -2
- package/app/server/auth/index.ts +1 -1
- package/app/server/auth/providers/InMemoryProvider.ts +1 -1
- package/app/server/index.ts +3 -4
- package/app/server/live/LiveAdminPanel.ts +8 -8
- package/app/server/live/LiveForm.ts +1 -1
- package/app/server/live/LiveProtectedChat.ts +5 -5
- package/app/server/live/LiveRoomChat.ts +50 -28
- package/app/server/live/LiveUpload.ts +17 -3
- package/app/server/live/auto-generated-components.ts +26 -0
- package/app/server/live/rooms/ChatRoom.ts +17 -2
- package/app/server/routes/auth.routes.ts +29 -20
- package/app/server/routes/index.ts +9 -0
- package/app/server/routes/room.routes.ts +6 -6
- package/config/index.ts +3 -3
- package/config/system/app.config.ts +1 -1
- package/config/system/auth.config.ts +1 -1
- package/config/system/build.config.ts +8 -6
- package/config/system/client.config.ts +6 -4
- package/config/system/database.config.ts +1 -1
- package/config/system/logger.config.ts +1 -1
- package/config/system/monitoring.config.ts +6 -4
- package/config/system/plugins.config.ts +1 -1
- package/config/system/runtime.config.ts +1 -1
- package/config/system/server.config.ts +1 -1
- package/config/system/services.config.ts +1 -1
- package/config/system/session.config.ts +3 -3
- package/config/system/system.config.ts +1 -1
- package/core/build/vite-plugins.ts +3 -2
- package/core/cli/generators/plugin.ts +1 -1
- package/core/config/index.ts +8 -1
- package/core/framework/server.ts +9 -5
- package/core/index.ts +1 -1
- package/core/plugins/index.ts +1 -1
- package/core/plugins/manager.ts +5 -1
- package/core/plugins/types.ts +17 -1
- package/core/server/index.ts +5 -2
- package/core/server/live/index.ts +8 -71
- package/core/server/plugin-client-hooks.ts +97 -0
- package/core/types/types.ts +1 -0
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +8 -5
- package/src/client/components/ui/StatusBadge.tsx +23 -0
- package/core/utils/config-schema.ts +0 -480
- package/core/utils/env.ts +0 -305
- package/plugins/crypto-auth/README.md +0 -788
- package/plugins/crypto-auth/ai-context.md +0 -1282
- package/plugins/crypto-auth/cli/make-protected-route.command.ts +0 -383
- package/plugins/crypto-auth/client/CryptoAuthClient.ts +0 -302
- package/plugins/crypto-auth/client/components/AuthProvider.tsx +0 -131
- package/plugins/crypto-auth/client/components/LoginButton.tsx +0 -138
- package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +0 -89
- package/plugins/crypto-auth/client/components/index.ts +0 -12
- package/plugins/crypto-auth/client/index.ts +0 -12
- package/plugins/crypto-auth/config/index.ts +0 -34
- package/plugins/crypto-auth/index.ts +0 -173
- package/plugins/crypto-auth/package.json +0 -66
- package/plugins/crypto-auth/server/AuthMiddleware.ts +0 -181
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +0 -58
- package/plugins/crypto-auth/server/CryptoAuthService.ts +0 -186
- package/plugins/crypto-auth/server/index.ts +0 -25
- package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +0 -66
- package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +0 -26
- package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +0 -77
- package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +0 -45
- package/plugins/crypto-auth/server/middlewares/helpers.ts +0 -155
- package/plugins/crypto-auth/server/middlewares/index.ts +0 -22
- package/plugins/crypto-auth/server/middlewares.ts +0 -19
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// RoomChatDemo - Chat multi-salas with password-protected rooms
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useRef, useMemo, useReducer } from 'react'
|
|
4
4
|
import { Live } from '@/core/client'
|
|
5
5
|
import { LiveRoomChat } from '@server/live/LiveRoomChat'
|
|
6
6
|
|
|
@@ -10,13 +10,57 @@ const DEFAULT_ROOMS = [
|
|
|
10
10
|
{ id: 'random', name: 'Random' },
|
|
11
11
|
]
|
|
12
12
|
|
|
13
|
+
// Consolidated UI state to avoid fragmented useState calls and ensure
|
|
14
|
+
// related modal states update atomically.
|
|
15
|
+
interface ChatUIState {
|
|
16
|
+
text: string
|
|
17
|
+
error: string
|
|
18
|
+
createModal: { open: boolean; name: string; password: string }
|
|
19
|
+
passwordPrompt: { roomId: string; roomName: string; input: string } | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ChatUIAction =
|
|
23
|
+
| { type: 'SET_TEXT'; text: string }
|
|
24
|
+
| { type: 'SET_ERROR'; error: string }
|
|
25
|
+
| { type: 'OPEN_CREATE_MODAL' }
|
|
26
|
+
| { type: 'CLOSE_CREATE_MODAL' }
|
|
27
|
+
| { type: 'UPDATE_CREATE_FORM'; name?: string; password?: string }
|
|
28
|
+
| { type: 'OPEN_PASSWORD_PROMPT'; roomId: string; roomName: string }
|
|
29
|
+
| { type: 'CLOSE_PASSWORD_PROMPT' }
|
|
30
|
+
| { type: 'SET_PASSWORD_INPUT'; input: string }
|
|
31
|
+
|
|
32
|
+
function chatUIReducer(state: ChatUIState, action: ChatUIAction): ChatUIState {
|
|
33
|
+
switch (action.type) {
|
|
34
|
+
case 'SET_TEXT':
|
|
35
|
+
return { ...state, text: action.text }
|
|
36
|
+
case 'SET_ERROR':
|
|
37
|
+
return { ...state, error: action.error }
|
|
38
|
+
case 'OPEN_CREATE_MODAL':
|
|
39
|
+
return { ...state, createModal: { open: true, name: '', password: '' } }
|
|
40
|
+
case 'CLOSE_CREATE_MODAL':
|
|
41
|
+
return { ...state, createModal: { open: false, name: '', password: '' } }
|
|
42
|
+
case 'UPDATE_CREATE_FORM':
|
|
43
|
+
return { ...state, createModal: { ...state.createModal, name: action.name ?? state.createModal.name, password: action.password ?? state.createModal.password } }
|
|
44
|
+
case 'OPEN_PASSWORD_PROMPT':
|
|
45
|
+
return { ...state, passwordPrompt: { roomId: action.roomId, roomName: action.roomName, input: '' } }
|
|
46
|
+
case 'CLOSE_PASSWORD_PROMPT':
|
|
47
|
+
return { ...state, passwordPrompt: null }
|
|
48
|
+
case 'SET_PASSWORD_INPUT':
|
|
49
|
+
return state.passwordPrompt ? { ...state, passwordPrompt: { ...state.passwordPrompt, input: action.input } } : state
|
|
50
|
+
default:
|
|
51
|
+
return state
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const initialUIState: ChatUIState = {
|
|
56
|
+
text: '',
|
|
57
|
+
error: '',
|
|
58
|
+
createModal: { open: false, name: '', password: '' },
|
|
59
|
+
passwordPrompt: null,
|
|
60
|
+
}
|
|
61
|
+
|
|
13
62
|
export function RoomChatDemo() {
|
|
14
|
-
const [
|
|
15
|
-
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
16
|
-
const [passwordPrompt, setPasswordPrompt] = useState<{ roomId: string; roomName: string } | null>(null)
|
|
17
|
-
const [passwordInput, setPasswordInput] = useState('')
|
|
18
|
-
const [createForm, setCreateForm] = useState({ name: '', password: '' })
|
|
19
|
-
const [error, setError] = useState('')
|
|
63
|
+
const [ui, dispatch] = useReducer(chatUIReducer, initialUIState)
|
|
20
64
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
21
65
|
|
|
22
66
|
const defaultUsername = useMemo(() => {
|
|
@@ -37,11 +81,11 @@ export function RoomChatDemo() {
|
|
|
37
81
|
}, [activeMessages.length])
|
|
38
82
|
|
|
39
83
|
useEffect(() => {
|
|
40
|
-
if (error) {
|
|
41
|
-
const t = setTimeout(() =>
|
|
84
|
+
if (ui.error) {
|
|
85
|
+
const t = setTimeout(() => dispatch({ type: 'SET_ERROR', error: '' }), 3000)
|
|
42
86
|
return () => clearTimeout(t)
|
|
43
87
|
}
|
|
44
|
-
}, [error])
|
|
88
|
+
}, [ui.error])
|
|
45
89
|
|
|
46
90
|
const joinedRoomIds = chat.$state.rooms.map(r => r.id)
|
|
47
91
|
const joinedRoomsMap = new Map(chat.$state.rooms.map(r => [r.id, r]))
|
|
@@ -54,8 +98,7 @@ export function RoomChatDemo() {
|
|
|
54
98
|
|
|
55
99
|
// If the room is known to be private, prompt for password
|
|
56
100
|
if (isPrivate) {
|
|
57
|
-
|
|
58
|
-
setPasswordInput('')
|
|
101
|
+
dispatch({ type: 'OPEN_PASSWORD_PROMPT', roomId, roomName })
|
|
59
102
|
return
|
|
60
103
|
}
|
|
61
104
|
|
|
@@ -63,28 +106,26 @@ export function RoomChatDemo() {
|
|
|
63
106
|
const result = await chat.joinRoom({ roomId, roomName })
|
|
64
107
|
if (result && !result.success) {
|
|
65
108
|
// If rejected, might be password-protected — prompt
|
|
66
|
-
|
|
67
|
-
setPasswordInput('')
|
|
109
|
+
dispatch({ type: 'OPEN_PASSWORD_PROMPT', roomId, roomName })
|
|
68
110
|
}
|
|
69
111
|
}
|
|
70
112
|
|
|
71
113
|
const handlePasswordSubmit = async () => {
|
|
72
|
-
if (!passwordPrompt) return
|
|
114
|
+
if (!ui.passwordPrompt) return
|
|
73
115
|
const result = await chat.joinRoom({
|
|
74
|
-
roomId: passwordPrompt.roomId,
|
|
75
|
-
roomName: passwordPrompt.roomName,
|
|
76
|
-
password:
|
|
116
|
+
roomId: ui.passwordPrompt.roomId,
|
|
117
|
+
roomName: ui.passwordPrompt.roomName,
|
|
118
|
+
password: ui.passwordPrompt.input
|
|
77
119
|
})
|
|
78
120
|
if (result && !result.success) {
|
|
79
|
-
|
|
121
|
+
dispatch({ type: 'SET_ERROR', error: result.error || 'Senha incorreta' })
|
|
80
122
|
} else {
|
|
81
|
-
|
|
82
|
-
setPasswordInput('')
|
|
123
|
+
dispatch({ type: 'CLOSE_PASSWORD_PROMPT' })
|
|
83
124
|
}
|
|
84
125
|
}
|
|
85
126
|
|
|
86
127
|
const handleCreateRoom = async () => {
|
|
87
|
-
const name =
|
|
128
|
+
const name = ui.createModal.name.trim()
|
|
88
129
|
if (!name) return
|
|
89
130
|
const roomId = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
|
|
90
131
|
if (!roomId) return
|
|
@@ -92,20 +133,19 @@ export function RoomChatDemo() {
|
|
|
92
133
|
const result = await chat.createRoom({
|
|
93
134
|
roomId,
|
|
94
135
|
roomName: name,
|
|
95
|
-
password:
|
|
136
|
+
password: ui.createModal.password || undefined
|
|
96
137
|
})
|
|
97
138
|
if (result && !result.success) {
|
|
98
|
-
|
|
139
|
+
dispatch({ type: 'SET_ERROR', error: result.error || 'Falha ao criar sala' })
|
|
99
140
|
} else {
|
|
100
|
-
|
|
101
|
-
setCreateForm({ name: '', password: '' })
|
|
141
|
+
dispatch({ type: 'CLOSE_CREATE_MODAL' })
|
|
102
142
|
}
|
|
103
143
|
}
|
|
104
144
|
|
|
105
145
|
const handleSendMessage = async () => {
|
|
106
|
-
if (!text.trim() || !activeRoom) return
|
|
107
|
-
await chat.sendMessage({ text })
|
|
108
|
-
|
|
146
|
+
if (!ui.text.trim() || !activeRoom) return
|
|
147
|
+
await chat.sendMessage({ text: ui.text })
|
|
148
|
+
dispatch({ type: 'SET_TEXT', text: '' })
|
|
109
149
|
}
|
|
110
150
|
|
|
111
151
|
// Combine default rooms + custom rooms from shared directory (visible to all users)
|
|
@@ -133,7 +173,7 @@ export function RoomChatDemo() {
|
|
|
133
173
|
<div className="flex items-center justify-between px-2 py-1">
|
|
134
174
|
<p className="text-xs text-gray-500">SALAS</p>
|
|
135
175
|
<button
|
|
136
|
-
onClick={() =>
|
|
176
|
+
onClick={() => dispatch({ type: 'OPEN_CREATE_MODAL' })}
|
|
137
177
|
className="text-xs text-purple-400 hover:text-purple-300"
|
|
138
178
|
>+ Criar</button>
|
|
139
179
|
</div>
|
|
@@ -223,15 +263,15 @@ export function RoomChatDemo() {
|
|
|
223
263
|
<div className="p-3 sm:p-4 border-t border-white/10">
|
|
224
264
|
<div className="flex gap-2">
|
|
225
265
|
<input
|
|
226
|
-
value={text}
|
|
227
|
-
onChange={(e) =>
|
|
266
|
+
value={ui.text}
|
|
267
|
+
onChange={(e) => dispatch({ type: 'SET_TEXT', text: e.target.value })}
|
|
228
268
|
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage() } }}
|
|
229
269
|
placeholder="Digite uma mensagem..."
|
|
230
270
|
className="flex-1 px-3 sm:px-4 py-2 rounded-xl bg-white/10 border border-white/20 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 text-sm sm:text-base"
|
|
231
271
|
/>
|
|
232
272
|
<button
|
|
233
273
|
onClick={handleSendMessage}
|
|
234
|
-
disabled={!text.trim()}
|
|
274
|
+
disabled={!ui.text.trim()}
|
|
235
275
|
className="px-4 sm:px-6 py-2 rounded-xl bg-purple-500/30 text-purple-200 hover:bg-purple-500/40 disabled:opacity-50 text-sm sm:text-base"
|
|
236
276
|
>Enviar</button>
|
|
237
277
|
</div>
|
|
@@ -248,23 +288,23 @@ export function RoomChatDemo() {
|
|
|
248
288
|
</div>
|
|
249
289
|
|
|
250
290
|
{/* Error toast */}
|
|
251
|
-
{error && (
|
|
291
|
+
{ui.error && (
|
|
252
292
|
<div className="absolute top-4 left-1/2 -translate-x-1/2 px-4 py-2 bg-red-500/90 text-white text-sm rounded-lg shadow-lg z-50">
|
|
253
|
-
{error}
|
|
293
|
+
{ui.error}
|
|
254
294
|
</div>
|
|
255
295
|
)}
|
|
256
296
|
|
|
257
297
|
{/* Create Room Modal */}
|
|
258
|
-
{
|
|
259
|
-
<div className="absolute inset-0 bg-black/60 flex items-center justify-center z-40" onClick={() =>
|
|
298
|
+
{ui.createModal.open && (
|
|
299
|
+
<div className="absolute inset-0 bg-black/60 flex items-center justify-center z-40" onClick={() => dispatch({ type: 'CLOSE_CREATE_MODAL' })}>
|
|
260
300
|
<div className="bg-gray-800 rounded-2xl p-6 w-80 border border-white/10" onClick={e => e.stopPropagation()}>
|
|
261
301
|
<h3 className="text-white font-bold text-lg mb-4">Criar Sala</h3>
|
|
262
302
|
<div className="space-y-3">
|
|
263
303
|
<div>
|
|
264
304
|
<label className="text-xs text-gray-400 block mb-1">Nome da sala</label>
|
|
265
305
|
<input
|
|
266
|
-
value={
|
|
267
|
-
onChange={e =>
|
|
306
|
+
value={ui.createModal.name}
|
|
307
|
+
onChange={e => dispatch({ type: 'UPDATE_CREATE_FORM', name: e.target.value })}
|
|
268
308
|
placeholder="Minha Sala"
|
|
269
309
|
className="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 text-sm"
|
|
270
310
|
autoFocus
|
|
@@ -275,8 +315,8 @@ export function RoomChatDemo() {
|
|
|
275
315
|
<label className="text-xs text-gray-400 block mb-1">Senha (opcional)</label>
|
|
276
316
|
<input
|
|
277
317
|
type="password"
|
|
278
|
-
value={
|
|
279
|
-
onChange={e =>
|
|
318
|
+
value={ui.createModal.password}
|
|
319
|
+
onChange={e => dispatch({ type: 'UPDATE_CREATE_FORM', password: e.target.value })}
|
|
280
320
|
placeholder="Deixe vazio para sala publica"
|
|
281
321
|
className="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 text-sm"
|
|
282
322
|
onKeyDown={e => { if (e.key === 'Enter') handleCreateRoom() }}
|
|
@@ -284,12 +324,12 @@ export function RoomChatDemo() {
|
|
|
284
324
|
</div>
|
|
285
325
|
<div className="flex gap-2 pt-2">
|
|
286
326
|
<button
|
|
287
|
-
onClick={() =>
|
|
327
|
+
onClick={() => dispatch({ type: 'CLOSE_CREATE_MODAL' })}
|
|
288
328
|
className="flex-1 px-4 py-2 rounded-lg bg-white/10 text-gray-300 hover:bg-white/20 text-sm"
|
|
289
329
|
>Cancelar</button>
|
|
290
330
|
<button
|
|
291
331
|
onClick={handleCreateRoom}
|
|
292
|
-
disabled={!
|
|
332
|
+
disabled={!ui.createModal.name.trim()}
|
|
293
333
|
className="flex-1 px-4 py-2 rounded-lg bg-purple-500/30 text-purple-200 hover:bg-purple-500/40 disabled:opacity-50 text-sm"
|
|
294
334
|
>Criar</button>
|
|
295
335
|
</div>
|
|
@@ -299,17 +339,17 @@ export function RoomChatDemo() {
|
|
|
299
339
|
)}
|
|
300
340
|
|
|
301
341
|
{/* Password Prompt Modal */}
|
|
302
|
-
{passwordPrompt && (
|
|
303
|
-
<div className="absolute inset-0 bg-black/60 flex items-center justify-center z-40" onClick={() =>
|
|
342
|
+
{ui.passwordPrompt && (
|
|
343
|
+
<div className="absolute inset-0 bg-black/60 flex items-center justify-center z-40" onClick={() => dispatch({ type: 'CLOSE_PASSWORD_PROMPT' })}>
|
|
304
344
|
<div className="bg-gray-800 rounded-2xl p-6 w-80 border border-white/10" onClick={e => e.stopPropagation()}>
|
|
305
345
|
<h3 className="text-white font-bold text-lg mb-1">Sala Protegida</h3>
|
|
306
346
|
<p className="text-sm text-gray-400 mb-4">
|
|
307
|
-
A sala "{passwordPrompt.roomName}" requer senha.
|
|
347
|
+
A sala "{ui.passwordPrompt.roomName}" requer senha.
|
|
308
348
|
</p>
|
|
309
349
|
<input
|
|
310
350
|
type="password"
|
|
311
|
-
value={
|
|
312
|
-
onChange={e =>
|
|
351
|
+
value={ui.passwordPrompt.input}
|
|
352
|
+
onChange={e => dispatch({ type: 'SET_PASSWORD_INPUT', input: e.target.value })}
|
|
313
353
|
placeholder="Digite a senha..."
|
|
314
354
|
className="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 text-sm mb-3"
|
|
315
355
|
autoFocus
|
|
@@ -317,12 +357,12 @@ export function RoomChatDemo() {
|
|
|
317
357
|
/>
|
|
318
358
|
<div className="flex gap-2">
|
|
319
359
|
<button
|
|
320
|
-
onClick={() =>
|
|
360
|
+
onClick={() => dispatch({ type: 'CLOSE_PASSWORD_PROMPT' })}
|
|
321
361
|
className="flex-1 px-4 py-2 rounded-lg bg-white/10 text-gray-300 hover:bg-white/20 text-sm"
|
|
322
362
|
>Cancelar</button>
|
|
323
363
|
<button
|
|
324
364
|
onClick={handlePasswordSubmit}
|
|
325
|
-
disabled={!
|
|
365
|
+
disabled={!ui.passwordPrompt.input}
|
|
326
366
|
className="flex-1 px-4 py-2 rounded-lg bg-purple-500/30 text-purple-200 hover:bg-purple-500/40 disabled:opacity-50 text-sm"
|
|
327
367
|
>Entrar</button>
|
|
328
368
|
</div>
|
|
@@ -32,6 +32,11 @@ export function SharedCounterDemo() {
|
|
|
32
32
|
|
|
33
33
|
useEffect(() => {
|
|
34
34
|
const unsub = counter.$room<CounterRoom>('counter:global').on('counter:updated', (data) => {
|
|
35
|
+
// Validate incoming room event data
|
|
36
|
+
if (data == null || typeof data.count !== 'number' || typeof data.updatedBy !== 'string') {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
const id = ++floatIdRef.current
|
|
36
41
|
const isReset = data.count === 0
|
|
37
42
|
const text = isReset ? '0' : data.count > 0 ? `+${data.count}` : `${data.count}`
|
|
@@ -48,6 +48,8 @@ export interface ProviderConfig {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export class AuthManager {
|
|
51
|
+
private static readonly MAX_GUARDS_CACHE = 100
|
|
52
|
+
|
|
51
53
|
private config: AuthManagerConfig
|
|
52
54
|
private guards = new Map<string, Guard>()
|
|
53
55
|
private customGuardFactories = new Map<string, GuardFactory>()
|
|
@@ -72,6 +74,15 @@ export class AuthManager {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
const guard = this.resolve(guardName)
|
|
77
|
+
|
|
78
|
+
// Evict oldest entries if cache exceeds limit (LRU-like)
|
|
79
|
+
if (this.guards.size >= AuthManager.MAX_GUARDS_CACHE) {
|
|
80
|
+
const firstKey = this.guards.keys().next().value
|
|
81
|
+
if (firstKey !== undefined) {
|
|
82
|
+
this.guards.delete(firstKey)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
this.guards.set(guardName, guard)
|
|
76
87
|
return guard
|
|
77
88
|
}
|
|
@@ -143,6 +154,19 @@ export class AuthManager {
|
|
|
143
154
|
return this.config
|
|
144
155
|
}
|
|
145
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Retorna um provider registrado por nome, ou undefined se não encontrado.
|
|
159
|
+
* Útil para acessar providers diretamente (ex: register route precisa de createUser).
|
|
160
|
+
*/
|
|
161
|
+
getProvider(name: string): UserProvider | undefined {
|
|
162
|
+
return this.providerInstances.get(name)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Retorna o tamanho atual do cache de guards */
|
|
166
|
+
getGuardsCacheSize(): number {
|
|
167
|
+
return this.guards.size
|
|
168
|
+
}
|
|
169
|
+
|
|
146
170
|
/** Resolve um guard por nome */
|
|
147
171
|
private resolve(name: string): Guard {
|
|
148
172
|
const guardConfig = this.config.guards[name]
|
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
* Adaptado para API-only (sem HTML, sem redirects, sem CSRF).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
// ===== Authenticatable JSON =====
|
|
13
|
+
|
|
14
|
+
/** Serialized user shape returned by toJSON() — matches Elysia response schemas */
|
|
15
|
+
export interface AuthenticatableJSON {
|
|
16
|
+
id: string | number
|
|
17
|
+
name?: string
|
|
18
|
+
email?: string
|
|
19
|
+
createdAt?: string
|
|
20
|
+
[key: string]: unknown
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
// ===== Authenticatable =====
|
|
13
24
|
|
|
14
25
|
/**
|
|
@@ -41,7 +52,7 @@ export interface Authenticatable {
|
|
|
41
52
|
setRememberToken(token: string | null): void
|
|
42
53
|
|
|
43
54
|
/** Retorna dados serializáveis do usuário (para response da API) */
|
|
44
|
-
toJSON():
|
|
55
|
+
toJSON(): AuthenticatableJSON
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
// ===== UserProvider =====
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Auth - Custom Error Classes
|
|
3
|
+
*
|
|
4
|
+
* Typed errors for better error handling in auth routes.
|
|
5
|
+
* Replaces generic catch blocks with classified errors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validation error (422) - invalid input, duplicate email, etc.
|
|
10
|
+
*/
|
|
11
|
+
export class AuthValidationError extends Error {
|
|
12
|
+
readonly name = 'AuthValidationError' as const
|
|
13
|
+
readonly field?: string
|
|
14
|
+
|
|
15
|
+
constructor(message: string, field?: string) {
|
|
16
|
+
super(message)
|
|
17
|
+
this.field = field
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Server error (500) - provider failure, unexpected internal error.
|
|
23
|
+
*/
|
|
24
|
+
export class AuthServerError extends Error {
|
|
25
|
+
readonly name = 'AuthServerError' as const
|
|
26
|
+
|
|
27
|
+
constructor(message: string) {
|
|
28
|
+
super(message)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authentication failed (401) - invalid credentials.
|
|
34
|
+
*/
|
|
35
|
+
export class AuthenticationError extends Error {
|
|
36
|
+
readonly name = 'AuthenticationError' as const
|
|
37
|
+
|
|
38
|
+
constructor(message: string = 'Invalid credentials') {
|
|
39
|
+
super(message)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Classify an unknown error into a typed auth error.
|
|
45
|
+
*/
|
|
46
|
+
export function classifyAuthError(error: unknown): {
|
|
47
|
+
status: number
|
|
48
|
+
error: string
|
|
49
|
+
message: string
|
|
50
|
+
} {
|
|
51
|
+
if (error instanceof AuthValidationError) {
|
|
52
|
+
return {
|
|
53
|
+
status: 422,
|
|
54
|
+
error: 'ValidationError',
|
|
55
|
+
message: error.message,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (error instanceof AuthenticationError) {
|
|
60
|
+
return {
|
|
61
|
+
status: 401,
|
|
62
|
+
error: 'AuthenticationFailed',
|
|
63
|
+
message: error.message,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (error instanceof AuthServerError) {
|
|
68
|
+
return {
|
|
69
|
+
status: 500,
|
|
70
|
+
error: 'ServerError',
|
|
71
|
+
message: error.message,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generic/unknown errors - don't leak internals in production
|
|
76
|
+
const msg = error instanceof Error ? error.message : 'An unexpected error occurred'
|
|
77
|
+
return {
|
|
78
|
+
status: 500,
|
|
79
|
+
error: 'InternalError',
|
|
80
|
+
message: process.env.NODE_ENV === 'production'
|
|
81
|
+
? 'An unexpected error occurred'
|
|
82
|
+
: msg,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -36,6 +36,9 @@ export class TokenGuard implements Guard {
|
|
|
36
36
|
/** Cache do usuário para a request atual */
|
|
37
37
|
private resolvedUser: Authenticatable | null | undefined = undefined
|
|
38
38
|
|
|
39
|
+
/** Last generated token (stored temporarily for the login response) */
|
|
40
|
+
private _lastGeneratedToken: string | null = null
|
|
41
|
+
|
|
39
42
|
constructor(
|
|
40
43
|
name: string,
|
|
41
44
|
provider: UserProvider,
|
|
@@ -136,7 +139,7 @@ export class TokenGuard implements Guard {
|
|
|
136
139
|
this.resolvedUser = user
|
|
137
140
|
|
|
138
141
|
// Salvar token plain-text temporariamente para a response poder retorná-lo
|
|
139
|
-
|
|
142
|
+
this._lastGeneratedToken = token
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
async logout(): Promise<void> {
|
|
@@ -171,7 +174,7 @@ export class TokenGuard implements Guard {
|
|
|
171
174
|
|
|
172
175
|
/** Retorna o último token gerado (para a response após login) */
|
|
173
176
|
getLastGeneratedToken(): string | null {
|
|
174
|
-
return
|
|
177
|
+
return this._lastGeneratedToken ?? null
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
/** Extrai Bearer token do header Authorization */
|
package/app/server/auth/index.ts
CHANGED
|
@@ -138,7 +138,7 @@ export function initAuth(): {
|
|
|
138
138
|
// 6. Conectar middleware
|
|
139
139
|
setAuthManagerForMiddleware(authManagerInstance)
|
|
140
140
|
|
|
141
|
-
console.log(`🔐 Auth system initialized (guard: ${authConfig.defaults.guard}, hash: ${authConfig.passwords.hashAlgorithm})`)
|
|
141
|
+
if (process.env.NODE_ENV !== 'production') console.log(`🔐 Auth system initialized (guard: ${authConfig.defaults.guard}, hash: ${authConfig.passwords.hashAlgorithm})`)
|
|
142
142
|
|
|
143
143
|
return {
|
|
144
144
|
authManager: authManagerInstance,
|
package/app/server/index.ts
CHANGED
|
@@ -13,20 +13,19 @@
|
|
|
13
13
|
import { FluxStackFramework } from "@core/server"
|
|
14
14
|
import { vitePlugin } from "@core/plugins/built-in/vite"
|
|
15
15
|
import { swaggerPlugin } from "@core/plugins/built-in/swagger"
|
|
16
|
-
import { liveComponentsPlugin } from "@core/server/live"
|
|
16
|
+
import { liveComponentsPlugin, registerAuthProvider } from "@core/server/live"
|
|
17
17
|
import { appInstance } from "@server/app"
|
|
18
18
|
import { appConfig } from "@config"
|
|
19
19
|
|
|
20
20
|
// 🔒 Auth provider para Live Components
|
|
21
|
-
import { liveAuthManager } from "@core/server/live"
|
|
22
21
|
import { DevAuthProvider } from "./auth/DevAuthProvider"
|
|
23
22
|
|
|
24
23
|
// 🔐 Auth system (Guard + Provider, Laravel-inspired)
|
|
25
24
|
import { initAuth } from "@server/auth"
|
|
26
25
|
|
|
27
26
|
// Registrar provider de desenvolvimento (tokens simples para testes)
|
|
28
|
-
|
|
29
|
-
console.log('🔓 DevAuthProvider registered')
|
|
27
|
+
registerAuthProvider(new DevAuthProvider())
|
|
28
|
+
if (process.env.NODE_ENV !== 'production') console.log('🔓 DevAuthProvider registered')
|
|
30
29
|
|
|
31
30
|
// Inicializar sistema de autenticação
|
|
32
31
|
initAuth()
|
|
@@ -80,9 +80,9 @@ export class LiveAdminPanel extends LiveComponent<AdminPanelState> {
|
|
|
80
80
|
async getAuthInfo() {
|
|
81
81
|
return {
|
|
82
82
|
authenticated: this.$auth.authenticated,
|
|
83
|
-
userId: this.$auth.
|
|
84
|
-
roles: this.$auth.
|
|
85
|
-
permissions: this.$auth.
|
|
83
|
+
userId: this.$auth.session?.id,
|
|
84
|
+
roles: this.$auth.session?.roles || [],
|
|
85
|
+
permissions: this.$auth.session?.permissions || [],
|
|
86
86
|
isAdmin: this.$auth.hasRole('admin'),
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -93,12 +93,12 @@ export class LiveAdminPanel extends LiveComponent<AdminPanelState> {
|
|
|
93
93
|
*/
|
|
94
94
|
async init() {
|
|
95
95
|
this.setState({
|
|
96
|
-
currentUser: this.$auth.
|
|
97
|
-
currentRoles: this.$auth.
|
|
96
|
+
currentUser: this.$auth.session?.id || null,
|
|
97
|
+
currentRoles: this.$auth.session?.roles || [],
|
|
98
98
|
isAdmin: this.$auth.hasRole('admin'),
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
-
this.addAudit('LOGIN', this.$auth.
|
|
101
|
+
this.addAudit('LOGIN', this.$auth.session?.id || 'unknown')
|
|
102
102
|
|
|
103
103
|
return { success: true }
|
|
104
104
|
}
|
|
@@ -125,7 +125,7 @@ export class LiveAdminPanel extends LiveComponent<AdminPanelState> {
|
|
|
125
125
|
users: [...this.state.users, user],
|
|
126
126
|
})
|
|
127
127
|
|
|
128
|
-
this.addAudit('ADD_USER', this.$auth.
|
|
128
|
+
this.addAudit('ADD_USER', this.$auth.session?.id || 'unknown', user.name)
|
|
129
129
|
|
|
130
130
|
return { success: true, user }
|
|
131
131
|
}
|
|
@@ -144,7 +144,7 @@ export class LiveAdminPanel extends LiveComponent<AdminPanelState> {
|
|
|
144
144
|
users: this.state.users.filter(u => u.id !== payload.userId),
|
|
145
145
|
})
|
|
146
146
|
|
|
147
|
-
this.addAudit('DELETE_USER', this.$auth.
|
|
147
|
+
this.addAudit('DELETE_USER', this.$auth.session?.id || 'unknown', user.name)
|
|
148
148
|
|
|
149
149
|
return { success: true }
|
|
150
150
|
}
|
|
@@ -23,7 +23,7 @@ export class LiveForm extends LiveComponent<typeof LiveForm.defaultState> {
|
|
|
23
23
|
throw new Error('Nome e email são obrigatórios')
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
console.log(`📝 Form submitted:`, { name, email, message })
|
|
26
|
+
if (process.env.NODE_ENV !== 'production') console.log(`📝 Form submitted:`, { name, email, message })
|
|
27
27
|
|
|
28
28
|
this.setState({
|
|
29
29
|
submitted: true,
|
|
@@ -75,7 +75,7 @@ export class LiveProtectedChat extends LiveComponent<ProtectedChatState> {
|
|
|
75
75
|
this.$room(payload.room).join()
|
|
76
76
|
|
|
77
77
|
// 🔒 Usar $auth para identificar o usuário
|
|
78
|
-
const userId = this.$auth.
|
|
78
|
+
const userId = this.$auth.session?.id || this.userId || 'anonymous'
|
|
79
79
|
const isAdmin = this.$auth.hasRole('admin')
|
|
80
80
|
|
|
81
81
|
this.setState({
|
|
@@ -96,7 +96,7 @@ export class LiveProtectedChat extends LiveComponent<ProtectedChatState> {
|
|
|
96
96
|
|
|
97
97
|
const message: ChatMessage = {
|
|
98
98
|
id: Date.now(),
|
|
99
|
-
userId: this.$auth.
|
|
99
|
+
userId: this.$auth.session?.id || this.userId || 'unknown',
|
|
100
100
|
text: payload.text.trim(),
|
|
101
101
|
timestamp: Date.now(),
|
|
102
102
|
isAdmin: this.$auth.hasRole('admin'),
|
|
@@ -142,9 +142,9 @@ export class LiveProtectedChat extends LiveComponent<ProtectedChatState> {
|
|
|
142
142
|
async getAuthInfo() {
|
|
143
143
|
return {
|
|
144
144
|
authenticated: this.$auth.authenticated,
|
|
145
|
-
userId: this.$auth.
|
|
146
|
-
roles: this.$auth.
|
|
147
|
-
permissions: this.$auth.
|
|
145
|
+
userId: this.$auth.session?.id,
|
|
146
|
+
roles: this.$auth.session?.roles,
|
|
147
|
+
permissions: this.$auth.session?.permissions,
|
|
148
148
|
isAdmin: this.$auth.hasRole('admin'),
|
|
149
149
|
}
|
|
150
150
|
}
|