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.
Files changed (93) hide show
  1. package/LLMD/resources/live-auth.md +462 -465
  2. package/app/client/.live-stubs/LiveAdminPanel.js +15 -0
  3. package/app/client/.live-stubs/LiveCounter.js +9 -0
  4. package/app/client/.live-stubs/LiveForm.js +11 -0
  5. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  6. package/app/client/.live-stubs/LivePingPong.js +10 -0
  7. package/app/client/.live-stubs/LiveRoomChat.js +11 -0
  8. package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
  9. package/app/client/.live-stubs/LiveUpload.js +15 -0
  10. package/app/client/src/App.tsx +45 -3
  11. package/app/client/src/components/AppLayout.tsx +10 -1
  12. package/app/client/src/components/ErrorBoundary.tsx +117 -0
  13. package/app/client/src/components/LiveErrorBoundary.tsx +87 -0
  14. package/app/client/src/components/LiveUploadWidget.tsx +10 -14
  15. package/app/client/src/lib/eden-api.ts +6 -0
  16. package/app/client/src/lib/plugin-hooks.ts +82 -0
  17. package/app/client/src/live/AuthDemo.tsx +0 -1
  18. package/app/client/src/live/FormDemo.tsx +1 -1
  19. package/app/client/src/live/PingPongDemo.tsx +4 -1
  20. package/app/client/src/live/RoomChatDemo.tsx +90 -50
  21. package/app/client/src/live/SharedCounterDemo.tsx +5 -0
  22. package/app/server/auth/AuthManager.ts +24 -0
  23. package/app/server/auth/contracts.ts +12 -1
  24. package/app/server/auth/errors.ts +84 -0
  25. package/app/server/auth/guards/TokenGuard.ts +5 -2
  26. package/app/server/auth/index.ts +1 -1
  27. package/app/server/auth/providers/InMemoryProvider.ts +1 -1
  28. package/app/server/index.ts +3 -4
  29. package/app/server/live/LiveAdminPanel.ts +8 -8
  30. package/app/server/live/LiveForm.ts +1 -1
  31. package/app/server/live/LiveProtectedChat.ts +5 -5
  32. package/app/server/live/LiveRoomChat.ts +50 -28
  33. package/app/server/live/LiveUpload.ts +17 -3
  34. package/app/server/live/auto-generated-components.ts +26 -0
  35. package/app/server/live/rooms/ChatRoom.ts +17 -2
  36. package/app/server/routes/auth.routes.ts +29 -20
  37. package/app/server/routes/index.ts +9 -0
  38. package/app/server/routes/room.routes.ts +6 -6
  39. package/config/index.ts +3 -3
  40. package/config/system/app.config.ts +1 -1
  41. package/config/system/auth.config.ts +1 -1
  42. package/config/system/build.config.ts +8 -6
  43. package/config/system/client.config.ts +6 -4
  44. package/config/system/database.config.ts +1 -1
  45. package/config/system/logger.config.ts +1 -1
  46. package/config/system/monitoring.config.ts +6 -4
  47. package/config/system/plugins.config.ts +1 -1
  48. package/config/system/runtime.config.ts +1 -1
  49. package/config/system/server.config.ts +1 -1
  50. package/config/system/services.config.ts +1 -1
  51. package/config/system/session.config.ts +3 -3
  52. package/config/system/system.config.ts +1 -1
  53. package/core/build/vite-plugins.ts +3 -2
  54. package/core/cli/generators/plugin.ts +1 -1
  55. package/core/config/index.ts +8 -1
  56. package/core/framework/server.ts +9 -5
  57. package/core/index.ts +1 -1
  58. package/core/plugins/index.ts +1 -1
  59. package/core/plugins/manager.ts +5 -1
  60. package/core/plugins/types.ts +17 -1
  61. package/core/server/index.ts +5 -2
  62. package/core/server/live/index.ts +8 -71
  63. package/core/server/plugin-client-hooks.ts +97 -0
  64. package/core/types/types.ts +1 -0
  65. package/core/utils/version.ts +1 -1
  66. package/create-fluxstack.ts +1 -1
  67. package/package.json +8 -5
  68. package/src/client/components/ui/StatusBadge.tsx +23 -0
  69. package/core/utils/config-schema.ts +0 -480
  70. package/core/utils/env.ts +0 -305
  71. package/plugins/crypto-auth/README.md +0 -788
  72. package/plugins/crypto-auth/ai-context.md +0 -1282
  73. package/plugins/crypto-auth/cli/make-protected-route.command.ts +0 -383
  74. package/plugins/crypto-auth/client/CryptoAuthClient.ts +0 -302
  75. package/plugins/crypto-auth/client/components/AuthProvider.tsx +0 -131
  76. package/plugins/crypto-auth/client/components/LoginButton.tsx +0 -138
  77. package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +0 -89
  78. package/plugins/crypto-auth/client/components/index.ts +0 -12
  79. package/plugins/crypto-auth/client/index.ts +0 -12
  80. package/plugins/crypto-auth/config/index.ts +0 -34
  81. package/plugins/crypto-auth/index.ts +0 -173
  82. package/plugins/crypto-auth/package.json +0 -66
  83. package/plugins/crypto-auth/server/AuthMiddleware.ts +0 -181
  84. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +0 -58
  85. package/plugins/crypto-auth/server/CryptoAuthService.ts +0 -186
  86. package/plugins/crypto-auth/server/index.ts +0 -25
  87. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +0 -66
  88. package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +0 -26
  89. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +0 -77
  90. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +0 -45
  91. package/plugins/crypto-auth/server/middlewares/helpers.ts +0 -155
  92. package/plugins/crypto-auth/server/middlewares/index.ts +0 -22
  93. 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 { useState, useEffect, useRef, useMemo } from 'react'
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 [text, setText] = useState('')
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(() => setError(''), 3000)
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
- setPasswordPrompt({ roomId, roomName })
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
- setPasswordPrompt({ roomId, roomName })
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: passwordInput
116
+ roomId: ui.passwordPrompt.roomId,
117
+ roomName: ui.passwordPrompt.roomName,
118
+ password: ui.passwordPrompt.input
77
119
  })
78
120
  if (result && !result.success) {
79
- setError(result.error || 'Senha incorreta')
121
+ dispatch({ type: 'SET_ERROR', error: result.error || 'Senha incorreta' })
80
122
  } else {
81
- setPasswordPrompt(null)
82
- setPasswordInput('')
123
+ dispatch({ type: 'CLOSE_PASSWORD_PROMPT' })
83
124
  }
84
125
  }
85
126
 
86
127
  const handleCreateRoom = async () => {
87
- const name = createForm.name.trim()
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: createForm.password || undefined
136
+ password: ui.createModal.password || undefined
96
137
  })
97
138
  if (result && !result.success) {
98
- setError(result.error || 'Falha ao criar sala')
139
+ dispatch({ type: 'SET_ERROR', error: result.error || 'Falha ao criar sala' })
99
140
  } else {
100
- setShowCreateModal(false)
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
- setText('')
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={() => { setShowCreateModal(true); setCreateForm({ name: '', password: '' }) }}
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) => setText(e.target.value)}
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
- {showCreateModal && (
259
- <div className="absolute inset-0 bg-black/60 flex items-center justify-center z-40" onClick={() => setShowCreateModal(false)}>
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={createForm.name}
267
- onChange={e => setCreateForm(f => ({ ...f, name: e.target.value }))}
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={createForm.password}
279
- onChange={e => setCreateForm(f => ({ ...f, password: e.target.value }))}
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={() => setShowCreateModal(false)}
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={!createForm.name.trim()}
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={() => setPasswordPrompt(null)}>
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={passwordInput}
312
- onChange={e => setPasswordInput(e.target.value)}
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={() => setPasswordPrompt(null)}
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={!passwordInput}
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(): Record<string, unknown>
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
- ;(this as any)._lastGeneratedToken = token
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 (this as any)._lastGeneratedToken ?? null
177
+ return this._lastGeneratedToken ?? null
175
178
  }
176
179
 
177
180
  /** Extrai Bearer token do header Authorization */
@@ -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,
@@ -62,7 +62,7 @@ export class InMemoryUser implements Authenticatable {
62
62
  this.rememberToken = token
63
63
  }
64
64
 
65
- toJSON(): Record<string, unknown> {
65
+ toJSON() {
66
66
  return {
67
67
  id: this.id,
68
68
  name: this.name,
@@ -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
- liveAuthManager.register(new DevAuthProvider())
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.user?.id,
84
- roles: this.$auth.user?.roles || [],
85
- permissions: this.$auth.user?.permissions || [],
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.user?.id || null,
97
- currentRoles: this.$auth.user?.roles || [],
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.user?.id || 'unknown')
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.user?.id || 'unknown', user.name)
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.user?.id || 'unknown', user.name)
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.user?.id || this.userId || 'anonymous'
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.user?.id || this.userId || 'unknown',
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.user?.id,
146
- roles: this.$auth.user?.roles,
147
- permissions: this.$auth.user?.permissions,
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
  }