Open the admin console from serve --open and land login on the intended page
This commit is contained in:
@@ -130,7 +130,7 @@ func runServe(args []string) error {
|
|||||||
AutoMigrate: true,
|
AutoMigrate: true,
|
||||||
}
|
}
|
||||||
if *open {
|
if *open {
|
||||||
openBrowser("http://" + *addr + "/ui")
|
openBrowser("http://" + *addr + "/ui/admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print the login once the server is about to start.
|
// Print the login once the server is about to start.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<p class="eyebrow">SciMesh</p>
|
<p class="eyebrow">SciMesh</p>
|
||||||
<h1>Sign in</h1>
|
<h1>Sign in</h1>
|
||||||
<form method="post" action="/ui/login">
|
<form method="post" action="/ui/login">
|
||||||
|
{{if .Next}}<input type="hidden" name="next" value="{{.Next}}">{{end}}
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
<input id="email" name="email" type="email" autocomplete="username" required autofocus>
|
<input id="email" name="email" type="email" autocomplete="username" required autofocus>
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emil28092005/SciMesh/coordinator/internal/authctx"
|
"github.com/emil28092005/SciMesh/coordinator/internal/authctx"
|
||||||
@@ -53,7 +55,7 @@ type tokenVerifier interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleUILoginForm(w http.ResponseWriter, r *http.Request) {
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
setSessionCookie(w, r, resp.Token)
|
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
|
// 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) {
|
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")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user