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