From fa67ce6346fa1c49bf4c981317193b3e14653d60 Mon Sep 17 00:00:00 2001 From: grm Date: Fri, 18 Sep 2026 11:24:43 +0300 Subject: Send a blogger to their own dashboard from another blog's /webadmin Typing /webadmin on someone else's blog while logged in (or logging in from there) landed on /b// and a 403 "This is not your blog." Both login paths now go through landing(), which swaps a next that points at a blog the user cannot manage for /dashboard; superadmins keep going where they asked. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- AGENTS.md | 5 ++++- internal/web/handlers_auth.go | 30 +++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2953025..679bb64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,7 +115,10 @@ internal/web/ server.go (host router, middleware, render helpers) `/news`, `/tag/go`) — `/news/?p=2` would be a 404. - **Auth**: login lives only at `/webadmin` on the root domain (deliberately not `/login`, and not linked from public pages); `/webadmin` on a blog host - redirects there with `next=/b//`. HS256 JWT in `session` cookie (`HttpOnly`, `SameSite=Lax`, + redirects there with `next=/b//`. After login (or when already logged + in) `landing` sends a blogger whose `next` is another blog's `/b//…` + to `/dashboard` (their own blog) instead of the 403; superadmins go where + they asked. HS256 JWT in `session` cookie (`HttpOnly`, `SameSite=Lax`, 7 days). Claims carry `uid` + `ver` (= `users.token_version`); the session middleware re-loads the user every request and drops the session if disabled or version mismatch. Password change / reset / disable bump diff --git a/internal/web/handlers_auth.go b/internal/web/handlers_auth.go index 3030f11..1321b2d 100644 --- a/internal/web/handlers_auth.go +++ b/internal/web/handlers_auth.go @@ -12,16 +12,31 @@ import ( func (s *Server) handleLoginForm(w http.ResponseWriter, r *http.Request) { next := safeNext(r.URL.Query().Get("next")) - if currentUser(r) != nil { - if next == "" { - next = "/dashboard" - } - http.Redirect(w, r, next, http.StatusSeeOther) + if u := currentUser(r); u != nil { + http.Redirect(w, r, s.landing(r, u, next), http.StatusSeeOther) return } s.render(w, r, "auth/login.html", map[string]any{"next": next}) } +// landing is where a user goes once logged in: next, unless that is another +// blogger's dashboard — someone typing /webadmin on a blog that is not theirs +// gets their own dashboard, not a 403. +func (s *Server) landing(r *http.Request, u *store.User, next string) string { + if next == "" { + return "/dashboard" + } + if rest, ok := strings.CutPrefix(next, "/b/"); ok && !u.IsSuperadmin() { + sub, _, _ := strings.Cut(rest, "/") + sub, _, _ = strings.Cut(sub, "?") + blog, err := s.st.BlogBySubdomain(r.Context(), sub) + if err != nil || blog.OwnerID != u.ID { + return "/dashboard" + } + } + return next +} + // handleWebadminRedirect serves /webadmin on a blog's own host: the login page // lives on the root domain, so bounce there and come back to this blog's dashboard. func (s *Server) handleWebadminRedirect(w http.ResponseWriter, r *http.Request) { @@ -57,10 +72,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { return } auth.SetSessionCookie(w, tok) - if next == "" { - next = "/dashboard" - } - http.Redirect(w, r, next, http.StatusSeeOther) + http.Redirect(w, r, s.landing(r, u, next), http.StatusSeeOther) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { -- cgit v1.2.3