MikelWL commited on
Commit
97a1a34
·
1 Parent(s): abaccfd

Fix HF login by using token auth fallback

Browse files
backend/api/main.py CHANGED
@@ -75,6 +75,12 @@ async def auth_middleware(request: Request, call_next):
75
  if request.headers.get(INTERNAL_HEADER) == password:
76
  return await call_next(request)
77
 
 
 
 
 
 
 
78
  token = request.cookies.get(COOKIE_NAME)
79
  if token and verify_session_token(token, password):
80
  return await call_next(request)
@@ -124,10 +130,14 @@ async def websocket_conversation_endpoint(websocket: WebSocket, conversation_id:
124
  if websocket.headers.get(INTERNAL_HEADER) == password:
125
  pass
126
  else:
127
- token = websocket.cookies.get(COOKIE_NAME)
128
- if not (token and verify_session_token(token, password)):
129
- await websocket.close(code=1008)
130
- return
 
 
 
 
131
  await websocket_endpoint(websocket, conversation_id)
132
 
133
 
 
75
  if request.headers.get(INTERNAL_HEADER) == password:
76
  return await call_next(request)
77
 
78
+ authz = request.headers.get("authorization")
79
+ if isinstance(authz, str) and authz.lower().startswith("bearer "):
80
+ token = authz.split(" ", 1)[1].strip()
81
+ if token and verify_session_token(token, password):
82
+ return await call_next(request)
83
+
84
  token = request.cookies.get(COOKIE_NAME)
85
  if token and verify_session_token(token, password):
86
  return await call_next(request)
 
130
  if websocket.headers.get(INTERNAL_HEADER) == password:
131
  pass
132
  else:
133
+ bearer = websocket.query_params.get("token")
134
+ if isinstance(bearer, str) and bearer and verify_session_token(bearer, password):
135
+ pass
136
+ else:
137
+ token = websocket.cookies.get(COOKIE_NAME)
138
+ if not (token and verify_session_token(token, password)):
139
+ await websocket.close(code=1008)
140
+ return
141
  await websocket_endpoint(websocket, conversation_id)
142
 
143
 
frontend/pages/config_view.py CHANGED
@@ -11,7 +11,7 @@ def get_config_view_js() -> str:
11
  const [savedAt, setSavedAt] = React.useState(existing?.saved_at || null);
12
 
13
  React.useEffect(() => {
14
- fetch('/api/personas')
15
  .then(r => r.json())
16
  .then(data => {
17
  setPersonas({
 
11
  const [savedAt, setSavedAt] = React.useState(existing?.saved_at || null);
12
 
13
  React.useEffect(() => {
14
+ authedFetch('/api/personas')
15
  .then(r => r.json())
16
  .then(data => {
17
  setPersonas({
frontend/pages/main_page.py CHANGED
@@ -1,7 +1,7 @@
1
  from .config_view import get_config_view_js
2
 
3
 
4
- def get_main_page_html() -> str:
5
  html = r"""<!DOCTYPE html>
6
  <html lang="en">
7
  <head>
@@ -20,6 +20,8 @@ def get_main_page_html() -> str:
20
  const { useState, useEffect, useRef } = React;
21
 
22
  const STORAGE_KEY = 'converta.config.v1';
 
 
23
 
24
  function loadConfig() {
25
  try {
@@ -31,6 +33,47 @@ def get_main_page_html() -> str:
31
  }
32
  }
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  function PageNav({ active, onChange }) {
35
  const base = "px-4 py-2 rounded-lg text-sm font-semibold border transition-colors";
36
  const activeCls = "bg-slate-900 text-white border-slate-900";
@@ -45,7 +88,65 @@ def get_main_page_html() -> str:
45
 
46
  /*__CONFIG_VIEW__*/
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  function App() {
 
49
  const [activePage, setActivePage] = useState('main');
50
  const [conversationActive, setConversationActive] = useState(false);
51
  const [messages, setMessages] = useState([]);
@@ -62,6 +163,28 @@ def get_main_page_html() -> str:
62
  const transcriptContainerRef = useRef(null);
63
  const stickToBottomRef = useRef(true);
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  useEffect(() => {
66
  return () => {
67
  if (wsRef.current) {
@@ -81,7 +204,9 @@ def get_main_page_html() -> str:
81
  const connectWebSocket = () => {
82
  conversationIdRef.current = `react_conv_${Date.now()}`;
83
  const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
84
- const wsUrl = `${wsScheme}://${window.location.host}/ws/frontend/${conversationIdRef.current}`;
 
 
85
 
86
  const websocket = new WebSocket(wsUrl);
87
 
@@ -163,6 +288,7 @@ def get_main_page_html() -> str:
163
  };
164
 
165
  const startConversation = () => {
 
166
  setMessages([]);
167
  setInsights([]);
168
  setRouting(null);
@@ -282,16 +408,16 @@ def get_main_page_html() -> str:
282
  <h1 className="text-2xl font-extrabold text-slate-900 tracking-tight">ConverTA</h1>
283
  </div>
284
 
285
- <div className="bg-white rounded-lg shadow-lg p-4 mb-6">
286
- <div className="flex items-center justify-between gap-4">
287
- <PageNav active={activePage} onChange={setActivePage} />
288
- {activePage === 'main' && (
289
- !conversationActive ? (
290
- <button onClick={startConversation} className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center gap-2 transition-all shadow">
291
- <span>⚡</span>
292
- Start
293
- </button>
294
- ) : (
295
  <button onClick={stopConversation} className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg text-sm font-semibold transition-all shadow">
296
  ⏹️ Stop
297
  </button>
@@ -510,6 +636,9 @@ def get_main_page_html() -> str:
510
  </div>
511
  )}
512
  </div>
 
 
 
513
  </div>
514
  );
515
  }
@@ -521,4 +650,6 @@ def get_main_page_html() -> str:
521
  </html>
522
  """
523
 
524
- return html.replace("/*__CONFIG_VIEW__*/", get_config_view_js())
 
 
 
1
  from .config_view import get_config_view_js
2
 
3
 
4
+ def get_main_page_html(auth_enabled: bool = False) -> str:
5
  html = r"""<!DOCTYPE html>
6
  <html lang="en">
7
  <head>
 
20
  const { useState, useEffect, useRef } = React;
21
 
22
  const STORAGE_KEY = 'converta.config.v1';
23
+ const SESSION_KEY = 'converta.session.v1';
24
+ const AUTH_ENABLED = /*__AUTH_ENABLED__*/ false;
25
 
26
  function loadConfig() {
27
  try {
 
33
  }
34
  }
35
 
36
+ function loadSessionToken() {
37
+ try {
38
+ const raw = localStorage.getItem(SESSION_KEY);
39
+ if (!raw) return null;
40
+ return raw;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function saveSessionToken(token) {
47
+ localStorage.setItem(SESSION_KEY, token);
48
+ }
49
+
50
+ function clearSessionToken() {
51
+ localStorage.removeItem(SESSION_KEY);
52
+ }
53
+
54
+ async function loginWithPassword(password) {
55
+ const res = await fetch('/login', {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({ password })
59
+ });
60
+ if (!res.ok) {
61
+ if (res.status === 401) throw new Error('Incorrect password.');
62
+ throw new Error('Login failed.');
63
+ }
64
+ const data = await res.json();
65
+ if (!data || !data.token) throw new Error('Login failed.');
66
+ saveSessionToken(data.token);
67
+ return data.token;
68
+ }
69
+
70
+ async function authedFetch(url, options) {
71
+ const token = loadSessionToken();
72
+ const headers = Object.assign({}, (options && options.headers) || {});
73
+ if (token) headers['Authorization'] = `Bearer ${token}`;
74
+ return fetch(url, Object.assign({}, options || {}, { headers }));
75
+ }
76
+
77
  function PageNav({ active, onChange }) {
78
  const base = "px-4 py-2 rounded-lg text-sm font-semibold border transition-colors";
79
  const activeCls = "bg-slate-900 text-white border-slate-900";
 
88
 
89
  /*__CONFIG_VIEW__*/
90
 
91
+ function LoginOverlay({ onAuthed }) {
92
+ const [pw, setPw] = useState('');
93
+ const [err, setErr] = useState(null);
94
+ const [busy, setBusy] = useState(false);
95
+
96
+ const submit = async () => {
97
+ setErr(null);
98
+ setBusy(true);
99
+ try {
100
+ const token = await loginWithPassword(pw);
101
+ onAuthed(token);
102
+ } catch (e) {
103
+ setErr(e?.message || 'Login failed.');
104
+ } finally {
105
+ setBusy(false);
106
+ }
107
+ };
108
+
109
+ return (
110
+ <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-6 z-50">
111
+ <div className="w-full max-w-md bg-white rounded-lg shadow-xl p-6">
112
+ <div className="flex items-center gap-3 mb-4">
113
+ <span className="text-3xl">🧠</span>
114
+ <div className="text-xl font-extrabold text-slate-900 tracking-tight">ConverTA</div>
115
+ </div>
116
+ <div className="text-sm text-slate-600">
117
+ Enter the shared password to unlock the app.
118
+ </div>
119
+
120
+ <div className="mt-4">
121
+ <label className="block text-sm font-semibold text-slate-700 mb-2">Password</label>
122
+ <input
123
+ type="password"
124
+ value={pw}
125
+ onChange={(e) => setPw(e.target.value)}
126
+ onKeyDown={(e) => { if (e.key === 'Enter') submit(); }}
127
+ className="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white"
128
+ autoFocus
129
+ />
130
+ {err && <div className="text-sm text-red-600 mt-2">{err}</div>}
131
+ </div>
132
+
133
+ <div className="mt-6 flex items-center justify-end">
134
+ <button
135
+ type="button"
136
+ onClick={submit}
137
+ disabled={busy}
138
+ className="bg-slate-900 hover:bg-slate-800 disabled:bg-slate-400 text-white px-4 py-2 rounded-lg text-sm font-semibold transition-all shadow"
139
+ >
140
+ {busy ? 'Checking…' : 'Unlock'}
141
+ </button>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ );
146
+ }
147
+
148
  function App() {
149
+ const [authenticated, setAuthenticated] = useState(!AUTH_ENABLED);
150
  const [activePage, setActivePage] = useState('main');
151
  const [conversationActive, setConversationActive] = useState(false);
152
  const [messages, setMessages] = useState([]);
 
163
  const transcriptContainerRef = useRef(null);
164
  const stickToBottomRef = useRef(true);
165
 
166
+ useEffect(() => {
167
+ if (!AUTH_ENABLED) return;
168
+ const token = loadSessionToken();
169
+ if (!token) {
170
+ setAuthenticated(false);
171
+ return;
172
+ }
173
+ authedFetch('/api/health')
174
+ .then((r) => {
175
+ if (r.ok) {
176
+ setAuthenticated(true);
177
+ } else {
178
+ clearSessionToken();
179
+ setAuthenticated(false);
180
+ }
181
+ })
182
+ .catch(() => {
183
+ clearSessionToken();
184
+ setAuthenticated(false);
185
+ });
186
+ }, []);
187
+
188
  useEffect(() => {
189
  return () => {
190
  if (wsRef.current) {
 
204
  const connectWebSocket = () => {
205
  conversationIdRef.current = `react_conv_${Date.now()}`;
206
  const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
207
+ const token = loadSessionToken();
208
+ const qs = token ? `?token=${encodeURIComponent(token)}` : '';
209
+ const wsUrl = `${wsScheme}://${window.location.host}/ws/frontend/${conversationIdRef.current}${qs}`;
210
 
211
  const websocket = new WebSocket(wsUrl);
212
 
 
288
  };
289
 
290
  const startConversation = () => {
291
+ if (AUTH_ENABLED && !authenticated) return;
292
  setMessages([]);
293
  setInsights([]);
294
  setRouting(null);
 
408
  <h1 className="text-2xl font-extrabold text-slate-900 tracking-tight">ConverTA</h1>
409
  </div>
410
 
411
+ <div className="bg-white rounded-lg shadow-lg p-4 mb-6">
412
+ <div className="flex items-center justify-between gap-4">
413
+ <PageNav active={activePage} onChange={setActivePage} />
414
+ {activePage === 'main' && (
415
+ !conversationActive ? (
416
+ <button onClick={startConversation} disabled={AUTH_ENABLED && !authenticated} className="bg-blue-600 hover:bg-blue-700 disabled:bg-slate-300 text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center gap-2 transition-all shadow">
417
+ <span>⚡</span>
418
+ Start
419
+ </button>
420
+ ) : (
421
  <button onClick={stopConversation} className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg text-sm font-semibold transition-all shadow">
422
  ⏹️ Stop
423
  </button>
 
636
  </div>
637
  )}
638
  </div>
639
+ {AUTH_ENABLED && !authenticated && (
640
+ <LoginOverlay onAuthed={() => setAuthenticated(true)} />
641
+ )}
642
  </div>
643
  );
644
  }
 
650
  </html>
651
  """
652
 
653
+ html = html.replace("/*__CONFIG_VIEW__*/", get_config_view_js())
654
+ html = html.replace("/*__AUTH_ENABLED__*/ false", "true" if auth_enabled else "false")
655
+ return html
frontend/react_gradio_hybrid.py CHANGED
@@ -17,7 +17,6 @@ sys.path.insert(0, str(project_root / "frontend"))
17
 
18
  from config.settings import get_settings
19
  from pages.main_page import get_main_page_html
20
- from pages.login_page import get_login_page_html
21
  from websocket_manager import WebSocketManager
22
  from backend.api.main import app as backend_app
23
  from backend.api.conversation_ws import manager as backend_ws_manager
@@ -80,15 +79,10 @@ def _is_request_authenticated(request: Request, password: str) -> bool:
80
 
81
  @app.get("/", response_class=HTMLResponse)
82
  async def serve_frontend(request: Request):
83
- """Serve login page (if locked) or the single-page React UI."""
84
  password = get_app_password()
85
- if not password:
86
- return HTMLResponse(content=get_main_page_html())
87
-
88
- if not _is_request_authenticated(request, password):
89
- return HTMLResponse(content=get_login_page_html(app_name="ConverTA"))
90
-
91
- return HTMLResponse(content=get_main_page_html())
92
 
93
 
94
  @app.post("/login")
@@ -104,7 +98,7 @@ async def login(request: Request, payload: dict):
104
  return JSONResponse({"error": "unauthorized"}, status_code=401)
105
 
106
  token = create_session_token(password)
107
- resp = JSONResponse({"ok": True})
108
  forwarded = request.headers.get("x-forwarded-proto")
109
  secure = (forwarded == "https") or (request.url.scheme == "https")
110
  resp.set_cookie(
@@ -132,7 +126,11 @@ async def frontend_websocket(websocket: WebSocket, conversation_id: str):
132
  password = get_app_password()
133
  if password:
134
  token = websocket.cookies.get(COOKIE_NAME)
135
- if not _is_authenticated_cookie(token, password):
 
 
 
 
136
  await websocket.close(code=1008)
137
  return
138
 
 
17
 
18
  from config.settings import get_settings
19
  from pages.main_page import get_main_page_html
 
20
  from websocket_manager import WebSocketManager
21
  from backend.api.main import app as backend_app
22
  from backend.api.conversation_ws import manager as backend_ws_manager
 
79
 
80
  @app.get("/", response_class=HTMLResponse)
81
  async def serve_frontend(request: Request):
82
+ """Serve the single-page React UI (conversation + configuration)."""
83
  password = get_app_password()
84
+ auth_enabled = bool(password)
85
+ return HTMLResponse(content=get_main_page_html(auth_enabled=auth_enabled))
 
 
 
 
 
86
 
87
 
88
  @app.post("/login")
 
98
  return JSONResponse({"error": "unauthorized"}, status_code=401)
99
 
100
  token = create_session_token(password)
101
+ resp = JSONResponse({"ok": True, "token": token})
102
  forwarded = request.headers.get("x-forwarded-proto")
103
  secure = (forwarded == "https") or (request.url.scheme == "https")
104
  resp.set_cookie(
 
126
  password = get_app_password()
127
  if password:
128
  token = websocket.cookies.get(COOKIE_NAME)
129
+ bearer = websocket.query_params.get("token")
130
+ authed = _is_authenticated_cookie(token, password) or (
131
+ isinstance(bearer, str) and verify_session_token(bearer, password)
132
+ )
133
+ if not authed:
134
  await websocket.close(code=1008)
135
  return
136