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.
- package/LLMD/resources/live-components.md +103 -57
- package/LLMD/resources/live-rooms.md +187 -88
- package/README.md +27 -25
- package/app/client/.live-stubs/LiveCounter.js +4 -4
- package/app/client/src/App.tsx +11 -12
- package/app/client/src/components/AppLayout.tsx +290 -252
- package/app/client/src/components/BackButton.tsx +16 -13
- package/app/client/src/components/DemoPage.tsx +135 -22
- package/app/client/src/index.css +21 -11
- package/app/client/src/live/AuthDemo.tsx +270 -333
- package/app/client/src/live/CounterDemo.tsx +151 -206
- package/app/client/src/live/FormDemo.tsx +140 -119
- package/app/client/src/live/PingPongDemo.tsx +180 -202
- package/app/client/src/live/RoomChatDemo.tsx +397 -374
- package/app/client/src/pages/HomePage.tsx +170 -104
- package/app/server/live/LiveCounter.ts +71 -68
- package/app/server/live/LiveSharedCounter.ts +18 -12
- package/app/server/live/auto-generated-components.ts +1 -3
- package/app/server/live/rooms/CounterRoom.ts +15 -10
- package/core/client/index.ts +0 -3
- package/core/client/state/createStore.ts +88 -88
- package/core/client/state/index.ts +5 -5
- package/core/server/live/auto-generated-components.ts +1 -3
- package/core/utils/version.ts +1 -1
- package/package.json +1 -1
- package/tsconfig.json +7 -6
- package/app/client/src/components/LiveUploadWidget.tsx +0 -200
- package/app/client/src/live/UploadDemo.tsx +0 -21
- package/app/server/live/LiveUpload.ts +0 -96
- package/core/client/hooks/useLiveUpload.ts +0 -70
|
@@ -1,333 +1,270 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<div className="
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
</
|
|
255
|
-
{
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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({ token })</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 = { required: true, roles: ['admin'] }</code></p>
|
|
325
|
-
<p><strong className="text-theme">Server:</strong> <code>static actionAuth = { deleteUser: { permissions: ['users.delete'] } }</code></p>
|
|
326
|
-
<p><strong className="text-theme">Server:</strong> <code>this.$auth.hasRole('admin')</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({ token })</code> para login</p>
|
|
329
|
-
<p><strong className="text-blue-300">Client:</strong> <code><LiveComponentsProvider auth={{ token }}></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
|
+
}
|