fluxy-bot 0.3.2 → 0.3.4
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/dist-fluxy/assets/{fluxy-Cejh7HRl.js → fluxy-DG-KxPWf.js} +29 -29
- package/dist-fluxy/fluxy.html +1 -1
- package/package.json +1 -1
- package/supervisor/chat/fluxy-main.tsx +10 -2
- package/supervisor/chat/src/components/LoginScreen.tsx +22 -7
- package/supervisor/chat/src/hooks/useFluxyChat.ts +6 -6
- package/supervisor/chat/src/lib/auth.ts +10 -2
- package/supervisor/index.ts +3 -2
- package/worker/index.ts +7 -5
package/dist-fluxy/fluxy.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Fluxy Chat</title>
|
|
7
|
-
<script type="module" crossorigin src="/fluxy/assets/fluxy-
|
|
7
|
+
<script type="module" crossorigin src="/fluxy/assets/fluxy-DG-KxPWf.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-Bu5tVsgN.js">
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-DYOj4b0m.css">
|
|
10
10
|
</head>
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { WsClient } from './src/lib/ws-client';
|
|
|
5
5
|
import { useFluxyChat } from './src/hooks/useFluxyChat';
|
|
6
6
|
import OnboardWizard from './OnboardWizard';
|
|
7
7
|
import LoginScreen from './src/components/LoginScreen';
|
|
8
|
-
import { getAuthToken, setAuthToken, clearAuthToken, authFetch } from './src/lib/auth';
|
|
8
|
+
import { getAuthToken, setAuthToken, clearAuthToken, authFetch, onAuthFailure } from './src/lib/auth';
|
|
9
9
|
import MessageList from './src/components/Chat/MessageList';
|
|
10
10
|
import InputBar from './src/components/Chat/InputBar';
|
|
11
11
|
import './src/styles/globals.css';
|
|
@@ -74,6 +74,14 @@ function FluxyApp() {
|
|
|
74
74
|
setAuthenticated(true);
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
// Handle mid-session token expiry (authFetch gets 401)
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
onAuthFailure(() => {
|
|
80
|
+
setAuthenticated(false);
|
|
81
|
+
setAuthRequired(true);
|
|
82
|
+
});
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
77
85
|
// Connect WebSocket only when authenticated
|
|
78
86
|
useEffect(() => {
|
|
79
87
|
if (!authenticated) return;
|
|
@@ -145,7 +153,7 @@ function FluxyApp() {
|
|
|
145
153
|
}, [menuOpen]);
|
|
146
154
|
|
|
147
155
|
const { messages, streaming, streamBuffer, tools, sendMessage, stopStreaming, clearContext } =
|
|
148
|
-
useFluxyChat(clientRef.current, reloadTrigger);
|
|
156
|
+
useFluxyChat(clientRef.current, reloadTrigger, authenticated);
|
|
149
157
|
|
|
150
158
|
// Auth gate: show spinner while checking, login screen if needed
|
|
151
159
|
if (!authChecked) {
|
|
@@ -6,12 +6,13 @@ interface Props {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export default function LoginScreen({ onLogin }: Props) {
|
|
9
|
+
const [username, setUsername] = useState('');
|
|
9
10
|
const [password, setPassword] = useState('');
|
|
10
11
|
const [error, setError] = useState('');
|
|
11
12
|
const [loading, setLoading] = useState(false);
|
|
12
13
|
|
|
13
14
|
const handleSubmit = async () => {
|
|
14
|
-
if (!password.trim() || loading) return;
|
|
15
|
+
if (!username.trim() || !password.trim() || loading) return;
|
|
15
16
|
setLoading(true);
|
|
16
17
|
setError('');
|
|
17
18
|
|
|
@@ -19,14 +20,14 @@ export default function LoginScreen({ onLogin }: Props) {
|
|
|
19
20
|
const res = await fetch('/api/portal/login', {
|
|
20
21
|
method: 'POST',
|
|
21
22
|
headers: { 'Content-Type': 'application/json' },
|
|
22
|
-
body: JSON.stringify({ password }),
|
|
23
|
+
body: JSON.stringify({ username: username.trim(), password }),
|
|
23
24
|
});
|
|
24
25
|
const data = await res.json();
|
|
25
26
|
|
|
26
27
|
if (res.ok && data.token) {
|
|
27
28
|
onLogin(data.token);
|
|
28
29
|
} else {
|
|
29
|
-
setError(data.error || 'Invalid
|
|
30
|
+
setError(data.error || 'Invalid credentials');
|
|
30
31
|
}
|
|
31
32
|
} catch {
|
|
32
33
|
setError('Could not reach server');
|
|
@@ -39,6 +40,8 @@ export default function LoginScreen({ onLogin }: Props) {
|
|
|
39
40
|
if (e.key === 'Enter') handleSubmit();
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
const inputCls = 'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none input-glow placeholder:text-white/20 transition-all';
|
|
44
|
+
|
|
42
45
|
return (
|
|
43
46
|
<div className="flex flex-col items-center justify-center h-dvh px-6">
|
|
44
47
|
<div className="w-full max-w-[320px] flex flex-col items-center">
|
|
@@ -50,7 +53,7 @@ export default function LoginScreen({ onLogin }: Props) {
|
|
|
50
53
|
Welcome back
|
|
51
54
|
</h1>
|
|
52
55
|
<p className="text-white/40 text-[13px] mb-6">
|
|
53
|
-
Enter your portal
|
|
56
|
+
Enter your portal credentials to continue.
|
|
54
57
|
</p>
|
|
55
58
|
|
|
56
59
|
{error && (
|
|
@@ -59,20 +62,32 @@ export default function LoginScreen({ onLogin }: Props) {
|
|
|
59
62
|
</div>
|
|
60
63
|
)}
|
|
61
64
|
|
|
65
|
+
<input
|
|
66
|
+
type="text"
|
|
67
|
+
value={username}
|
|
68
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
69
|
+
onKeyDown={handleKeyDown}
|
|
70
|
+
placeholder="Username"
|
|
71
|
+
autoFocus
|
|
72
|
+
autoComplete="username"
|
|
73
|
+
autoCapitalize="none"
|
|
74
|
+
autoCorrect="off"
|
|
75
|
+
className={inputCls}
|
|
76
|
+
/>
|
|
77
|
+
|
|
62
78
|
<input
|
|
63
79
|
type="password"
|
|
64
80
|
value={password}
|
|
65
81
|
onChange={(e) => setPassword(e.target.value)}
|
|
66
82
|
onKeyDown={handleKeyDown}
|
|
67
83
|
placeholder="Password"
|
|
68
|
-
autoFocus
|
|
69
84
|
autoComplete="current-password"
|
|
70
|
-
className=
|
|
85
|
+
className={inputCls + ' mt-3'}
|
|
71
86
|
/>
|
|
72
87
|
|
|
73
88
|
<button
|
|
74
89
|
onClick={handleSubmit}
|
|
75
|
-
disabled={!password.trim() || loading}
|
|
90
|
+
disabled={!username.trim() || !password.trim() || loading}
|
|
76
91
|
className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
|
|
77
92
|
>
|
|
78
93
|
{loading ? (
|
|
@@ -8,7 +8,7 @@ import { authFetch } from '../lib/auth';
|
|
|
8
8
|
* Loads/persists messages via the DB (worker API).
|
|
9
9
|
* Supports cross-device sync via chat:sync WS events.
|
|
10
10
|
*/
|
|
11
|
-
export function useFluxyChat(ws: WsClient | null, triggerReload?: number) {
|
|
11
|
+
export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enabled = true) {
|
|
12
12
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
13
13
|
const [conversationId, setConversationId] = useState<string | null>(null);
|
|
14
14
|
const [streaming, setStreaming] = useState(false);
|
|
@@ -62,19 +62,19 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number) {
|
|
|
62
62
|
} catch { /* worker not ready yet */ }
|
|
63
63
|
}, []);
|
|
64
64
|
|
|
65
|
-
// Load on mount
|
|
65
|
+
// Load on mount (only when enabled/authenticated)
|
|
66
66
|
useEffect(() => {
|
|
67
|
-
if (loaded.current) return;
|
|
67
|
+
if (!enabled || loaded.current) return;
|
|
68
68
|
loaded.current = true;
|
|
69
69
|
loadFromDb();
|
|
70
|
-
}, [loadFromDb]);
|
|
70
|
+
}, [enabled, loadFromDb]);
|
|
71
71
|
|
|
72
72
|
// Reload on reconnect (triggerReload changes)
|
|
73
73
|
useEffect(() => {
|
|
74
|
-
if (triggerReload && triggerReload > 0) {
|
|
74
|
+
if (enabled && triggerReload && triggerReload > 0) {
|
|
75
75
|
loadFromDb();
|
|
76
76
|
}
|
|
77
|
-
}, [triggerReload, loadFromDb]);
|
|
77
|
+
}, [enabled, triggerReload, loadFromDb]);
|
|
78
78
|
|
|
79
79
|
useEffect(() => {
|
|
80
80
|
if (!ws) return;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const TOKEN_KEY = 'fluxy_token';
|
|
2
2
|
|
|
3
|
+
let authFailureCallback: (() => void) | null = null;
|
|
4
|
+
|
|
3
5
|
export function getAuthToken(): string | null {
|
|
4
6
|
return localStorage.getItem(TOKEN_KEY);
|
|
5
7
|
}
|
|
@@ -12,6 +14,11 @@ export function clearAuthToken(): void {
|
|
|
12
14
|
localStorage.removeItem(TOKEN_KEY);
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
/** Register a callback for when a 401 is received (token expired mid-session) */
|
|
18
|
+
export function onAuthFailure(cb: () => void): void {
|
|
19
|
+
authFailureCallback = cb;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
export async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
|
16
23
|
const token = getAuthToken();
|
|
17
24
|
const headers = new Headers(options.headers);
|
|
@@ -21,9 +28,10 @@ export async function authFetch(url: string, options: RequestInit = {}): Promise
|
|
|
21
28
|
|
|
22
29
|
const res = await fetch(url, { ...options, headers });
|
|
23
30
|
|
|
24
|
-
if (res.status === 401) {
|
|
31
|
+
if (res.status === 401 && token) {
|
|
32
|
+
// Token was present but rejected — it expired
|
|
25
33
|
clearAuthToken();
|
|
26
|
-
|
|
34
|
+
authFailureCallback?.();
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
return res;
|
package/supervisor/index.ts
CHANGED
|
@@ -193,8 +193,9 @@ export async function startSupervisor() {
|
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
// Auth check for API routes
|
|
197
|
-
|
|
196
|
+
// Auth check for API mutation routes (POST/PUT/DELETE) — GET is open for dashboard
|
|
197
|
+
const method = req.method || 'GET';
|
|
198
|
+
if (method !== 'GET' && !isExemptRoute(method, req.url || '')) {
|
|
198
199
|
const needsAuth = await isAuthRequired();
|
|
199
200
|
if (needsAuth) {
|
|
200
201
|
const authHeader = req.headers['authorization'];
|
package/worker/index.ts
CHANGED
|
@@ -252,11 +252,13 @@ app.post('/api/portal/verify-password', (req, res) => {
|
|
|
252
252
|
});
|
|
253
253
|
|
|
254
254
|
app.post('/api/portal/login', (req, res) => {
|
|
255
|
-
const { password } = req.body;
|
|
256
|
-
if (!password) { res.status(400).json({ error: '
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
if (!
|
|
255
|
+
const { username, password } = req.body;
|
|
256
|
+
if (!username || !password) { res.status(400).json({ error: 'Username and password required' }); return; }
|
|
257
|
+
const storedUser = getSetting('portal_user');
|
|
258
|
+
const storedPass = getSetting('portal_pass');
|
|
259
|
+
if (!storedPass) { res.status(400).json({ error: 'No password set' }); return; }
|
|
260
|
+
if (username.trim().toLowerCase() !== storedUser) { res.status(401).json({ error: 'Invalid credentials' }); return; }
|
|
261
|
+
if (!verifyPassword(password, storedPass)) { res.status(401).json({ error: 'Invalid credentials' }); return; }
|
|
260
262
|
|
|
261
263
|
// Clean up expired sessions opportunistically
|
|
262
264
|
deleteExpiredSessions();
|