Open the admin console from serve --open and land login on the intended page

This commit is contained in:
Emil
2026-08-03 00:32:47 +03:00
parent bdbe0e3e39
commit 8b7094b0cb
4 changed files with 76 additions and 4 deletions
+1 -1
View File
@@ -130,7 +130,7 @@ func runServe(args []string) error {
AutoMigrate: true,
}
if *open {
openBrowser("http://" + *addr + "/ui")
openBrowser("http://" + *addr + "/ui/admin")
}
// Print the login once the server is about to start.
@@ -13,6 +13,7 @@
<p class="eyebrow">SciMesh</p>
<h1>Sign in</h1>
<form method="post" action="/ui/login">
{{if .Next}}<input type="hidden" name="next" value="{{.Next}}">{{end}}
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="username" required autofocus>
<label for="password">Password</label>
+21 -3
View File
@@ -6,6 +6,8 @@ import (
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/emil28092005/SciMesh/coordinator/internal/authctx"
@@ -53,7 +55,7 @@ type tokenVerifier interface {
}
func (s *Server) handleUILoginForm(w http.ResponseWriter, r *http.Request) {
s.renderUI(w, "login.html", map[string]any{"Error": r.URL.Query().Get("error")})
s.renderUI(w, "login.html", map[string]any{"Error": r.URL.Query().Get("error"), "Next": r.URL.Query().Get("next")})
}
func (s *Server) handleUIRegisterForm(w http.ResponseWriter, r *http.Request) {
@@ -85,7 +87,13 @@ func (s *Server) handleUILogin(w http.ResponseWriter, r *http.Request) {
return
}
setSessionCookie(w, r, resp.Token)
http.Redirect(w, r, "/ui", http.StatusSeeOther)
// Land back where the user was headed (e.g. /ui/admin); never follow a
// value that escapes the UI prefix — that would be an open redirect.
next := strings.TrimSpace(r.FormValue("next"))
if next == "" || !strings.HasPrefix(next, "/ui/") {
next = "/ui"
}
http.Redirect(w, r, next, http.StatusSeeOther)
}
// handleUIRegister creates an account through the userservice, then sends the
@@ -167,5 +175,15 @@ func clearSessionCookie(w http.ResponseWriter, r *http.Request) {
}
func redirectToLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/ui/login", http.StatusSeeOther)
// Remember where the user was headed so a successful login lands back
// there (e.g. /ui/admin) instead of the control room.
next := r.URL.Path
if !strings.HasPrefix(next, "/ui/") {
next = ""
}
target := "/ui/login"
if next != "" {
target += "?next=" + url.QueryEscape(next)
}
http.Redirect(w, r, target, http.StatusSeeOther)
}
@@ -173,3 +173,56 @@ func TestHandleUILogoutClearsCookie(t *testing.T) {
t.Error("logout must clear the session cookie")
}
}
func TestHandleUILoginRedirectsToNext(t *testing.T) {
stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"token":"t"}`))
}))
defer stub.Close()
s := newLoginServer(stub)
// A UI-scoped next is honoured: the admin lands back on the console.
rec := httptest.NewRecorder()
s.handleUILogin(rec, postForm("/ui/login", url.Values{"email": {"a@b.com"}, "password": {"p"}, "next": {"/ui/admin"}}))
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/ui/admin" {
t.Errorf("got %d -> %q, want 303 -> /ui/admin", rec.Code, rec.Header().Get("Location"))
}
// Anything outside the UI prefix must not become a redirect target.
for _, next := range []string{"https://evil.example", "/", "//evil.example", "/api/jobs"} {
rec = httptest.NewRecorder()
s.handleUILogin(rec, postForm("/ui/login", url.Values{"email": {"a@b.com"}, "password": {"p"}, "next": {next}}))
if loc := rec.Header().Get("Location"); loc != "/ui" {
t.Errorf("next=%q landed on %q, want /ui (no open redirect)", next, loc)
}
}
}
func TestRedirectToLoginCarriesNext(t *testing.T) {
rec := httptest.NewRecorder()
req := newReq(http.MethodGet, "/ui/admin", nil)
redirectToLogin(rec, req)
if loc := rec.Header().Get("Location"); loc != "/ui/login?next=%2Fui%2Fadmin" {
t.Errorf("location = %q, want /ui/login?next=%%2Fui%%2Fadmin", loc)
}
// Paths outside the UI stay on the plain login.
rec = httptest.NewRecorder()
req = newReq(http.MethodGet, "/health", nil)
redirectToLogin(rec, req)
if loc := rec.Header().Get("Location"); loc != "/ui/login" {
t.Errorf("location = %q, want /ui/login", loc)
}
}
func TestLoginFormRendersNext(t *testing.T) {
html := render(t, "login.html", map[string]any{"Next": "/ui/admin"})
if !strings.Contains(html, `name="next" value="/ui/admin"`) {
t.Error("login form must carry the next field")
}
html = render(t, "login.html", map[string]any{})
if strings.Contains(html, `name="next"`) {
t.Error("login form must not render next when absent")
}
}