diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 8a721f9..fd26694 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -48,6 +48,9 @@ func (s *Server) Start(ctx context.Context) error { mux := http.NewServeMux() // Minimal web UI mux.HandleFunc("/", s.handleUI) + mux.HandleFunc("/login", s.handleLogin) + mux.HandleFunc("/auth", s.handleAuth) + mux.HandleFunc("/logout", s.handleLogout) mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) @@ -206,6 +209,13 @@ func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) return } + // redirect to login if token cookie missing/invalid + if s.AuthToken != "" { + if c, err := r.Cookie("auth_token"); err != nil || c.Value != s.AuthToken { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + } w.Header().Set("Content-Type", "text/html; charset=utf-8") // Pico.css from CDN and a tiny app page := ` @@ -231,6 +241,7 @@ func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) { const url = new URL(path, window.location.origin); if(params && params.query){ Object.entries(params.query).forEach(([k,v])=>url.searchParams.set(k,v)); } const opts = { headers: {} }; + // use cookie for auth; header optional if present if(st.token){ opts.headers['Authorization'] = 'Bearer '+st.token; } const res = await fetch(url, opts); if(!res.ok){ throw new Error('HTTP '+res.status); } @@ -259,7 +270,9 @@ func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) { const lim = document.getElementById('limit').value || '100'; try{ const data = await api('/api/tail',{query:{channel:ch,limit:lim}}); const out = data.map(m => (m.time + ' ' + m.author + ': ' + m.body)).join('\n'); - document.getElementById('tail').textContent = out; + const el = document.getElementById('tail'); + el.textContent = out; + el.scrollTop = el.scrollHeight; }catch(e){ document.getElementById('tail').textContent = 'error: '+e; } st.tailLoading=false; } @@ -271,7 +284,9 @@ func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) { const prog = document.getElementById('summProg'); btn.disabled = true; prog.style.display = 'inline-block'; try{ const data = await api('/api/trigger',{query:{channel:ch,window:win,push:push}}); - document.getElementById('summary').textContent = data.summary || '(empty)'; + const el = document.getElementById('summary'); + el.textContent = data.summary || '(empty)'; + el.scrollTop = el.scrollHeight; }catch(e){ document.getElementById('summary').textContent = 'error: '+e; } btn.disabled = false; prog.style.display = 'none'; } @@ -292,10 +307,10 @@ func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) {