diff options
| author | grm <grm@eyesin.space> | 2026-09-12 12:15:47 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-12 12:15:47 +0300 |
| commit | 3073532f723b976a2f54666f779e9a045bacb7f6 (patch) | |
| tree | 442181bd2b5ac48a2ec5621203f81e64cb1769c5 /internal/web | |
| parent | f82c2256d619e92cf0e928deb3b23b735071aaf3 (diff) | |
| download | blogspace-3073532f723b976a2f54666f779e9a045bacb7f6.tar.gz blogspace-3073532f723b976a2f54666f779e9a045bacb7f6.tar.bz2 blogspace-3073532f723b976a2f54666f779e9a045bacb7f6.zip | |
Rename /login to /webadmin and reach it from every blog
The login URL is less guessable, bloggers can type /webadmin on their own
blog and get bounced to the root login page (and back to their dashboard
after logging in), and the public root blog no longer advertises the
admin entry point in its footer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web')
| -rw-r--r-- | internal/web/handlers_auth.go | 17 | ||||
| -rw-r--r-- | internal/web/handlers_blog.go | 3 | ||||
| -rw-r--r-- | internal/web/handlers_pages.go | 2 | ||||
| -rw-r--r-- | internal/web/routes.go | 6 | ||||
| -rw-r--r-- | internal/web/server.go | 2 | ||||
| -rw-r--r-- | internal/web/templates/auth/login.html | 2 | ||||
| -rw-r--r-- | internal/web/templates/layouts/blog.html | 2 | ||||
| -rw-r--r-- | internal/web/web_test.go | 23 |
8 files changed, 41 insertions, 16 deletions
diff --git a/internal/web/handlers_auth.go b/internal/web/handlers_auth.go index 5d82ead..7c1d1e8 100644 --- a/internal/web/handlers_auth.go +++ b/internal/web/handlers_auth.go @@ -11,11 +11,22 @@ import ( ) func (s *Server) handleLoginForm(w http.ResponseWriter, r *http.Request) { + next := safeNext(r.URL.Query().Get("next")) if currentUser(r) != nil { - http.Redirect(w, r, "/dashboard", http.StatusSeeOther) + if next == "" { + next = "/dashboard" + } + http.Redirect(w, r, next, http.StatusSeeOther) return } - s.render(w, r, "auth/login.html", map[string]any{"next": safeNext(r.URL.Query().Get("next"))}) + s.render(w, r, "auth/login.html", map[string]any{"next": 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) { + sub, _ := r.Context().Value(ctxHostSub).(string) + http.Redirect(w, r, s.cfg.RootURL()+"/webadmin?next="+urlQuery("/b/"+sub+"/"), http.StatusSeeOther) } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { @@ -54,7 +65,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { auth.ClearSessionCookie(w) - http.Redirect(w, r, "/login", http.StatusSeeOther) + http.Redirect(w, r, "/webadmin", http.StatusSeeOther) } func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/handlers_blog.go b/internal/web/handlers_blog.go index 27e67e6..b768c4d 100644 --- a/internal/web/handlers_blog.go +++ b/internal/web/handlers_blog.go @@ -7,7 +7,6 @@ import ( "strconv" "time" - "github.com/gramanas/blogspace/internal/config" "github.com/gramanas/blogspace/internal/store" ) @@ -27,7 +26,7 @@ func (s *Server) blogView(r *http.Request) (map[string]any, error) { nav = append(nav, p) } } - return map[string]any{"theme": theme, "css": theme.CSS(), "nav": nav, "isRoot": blog.Subdomain == config.RootSubdomain}, nil + return map[string]any{"theme": theme, "css": theme.CSS(), "nav": nav}, nil } func (s *Server) handleBlogHome(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/handlers_pages.go b/internal/web/handlers_pages.go index ac50513..db46097 100644 --- a/internal/web/handlers_pages.go +++ b/internal/web/handlers_pages.go @@ -13,7 +13,7 @@ import ( // Page slugs that would collide with blog routes, or with management routes on the root domain. var reservedPageSlugs = map[string]bool{"media": true, "static": true, "feed.xml": true, - "login": true, "logout": true, "dashboard": true, "account": true, "admin": true, "b": true, "healthz": true} + "webadmin": true, "logout": true, "dashboard": true, "account": true, "admin": true, "b": true, "healthz": true} func (s *Server) handlePages(w http.ResponseWriter, r *http.Request) { pages, err := s.st.ListPages(r.Context(), currentBlog(r).ID) diff --git a/internal/web/routes.go b/internal/web/routes.go index 02d536a..e135229 100644 --- a/internal/web/routes.go +++ b/internal/web/routes.go @@ -10,8 +10,8 @@ func urlQuery(s string) string { return url.QueryEscape(s) } func (s *Server) rootRoutes() http.Handler { m := http.NewServeMux() m.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) - m.HandleFunc("GET /login", s.handleLoginForm) - m.HandleFunc("POST /login", s.handleLogin) + m.HandleFunc("GET /webadmin", s.handleLoginForm) + m.HandleFunc("POST /webadmin", s.handleLogin) m.HandleFunc("POST /logout", s.handleLogout) m.HandleFunc("GET /dashboard", s.requireAuth(s.handleDashboard)) m.HandleFunc("GET /account/password", s.requireAuth(s.handlePasswordForm)) @@ -71,6 +71,8 @@ func (s *Server) blogRoutes(m *http.ServeMux, wrap func(http.HandlerFunc) http.H func (s *Server) subdomainRoutes() http.Handler { m := http.NewServeMux() + // Bloggers type /webadmin on their own blog; send them to the real login page. + m.HandleFunc("GET /webadmin", s.handleWebadminRedirect) m.HandleFunc("GET /media/{id}", s.handleMedia) m.Handle("GET /static/{file}", s.staticHandler()) s.blogRoutes(m, s.hostBlog) diff --git a/internal/web/server.go b/internal/web/server.go index 68dba3b..cbeac5d 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -129,7 +129,7 @@ func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { u := currentUser(r) if u == nil { - http.Redirect(w, r, "/login?next="+r.URL.Path, http.StatusSeeOther) + http.Redirect(w, r, "/webadmin?next="+r.URL.Path, http.StatusSeeOther) return } if r.Method == http.MethodPost { diff --git a/internal/web/templates/auth/login.html b/internal/web/templates/auth/login.html index 2229849..2aec3b1 100644 --- a/internal/web/templates/auth/login.html +++ b/internal/web/templates/auth/login.html @@ -2,7 +2,7 @@ {{define "content"}} <div class="card narrow"> <h1>Log in</h1> - <form method="post" action="/login"> + <form method="post" action="/webadmin"> <input type="hidden" name="next" value="{{.Data.next}}"> <label>Username<br><input name="username" value="{{.Data.username}}" required autofocus autocomplete="username"></label> <label>Password<br><input type="password" name="password" required autocomplete="current-password"></label> diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html index ff66c59..7e3128e 100644 --- a/internal/web/templates/layouts/blog.html +++ b/internal/web/templates/layouts/blog.html @@ -27,7 +27,7 @@ <div class="site-footer"> <div class="wrap footer-inner"> {{if .Data.theme.FooterText}}<p>{{.Data.theme.FooterText}}</p>{{end}} - <p class="small"><a href="/feed.xml">RSS</a> · {{.Blog.Title}}{{if .Data.isRoot}} · <a href="/login">Log in</a>{{end}}</p> + <p class="small"><a href="/feed.xml">RSS</a> · {{.Blog.Title}}</p> </div> </div> </body> diff --git a/internal/web/web_test.go b/internal/web/web_test.go index dcd766c..6e53bae 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -32,14 +32,14 @@ func TestHostRoutingWithoutDB(t *testing.T) { t.Errorf("host %q: got %d, want 404", host, rec.Code) } } - // root domain (and www) reach the management mux: /login renders without DB access + // root domain (and www) reach the management mux: /webadmin renders without DB access for _, host := range []string{"example.com", "www.example.com", "example.com:8080"} { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/login", nil) + req := httptest.NewRequest("GET", "/webadmin", nil) req.Host = host s.ServeHTTP(rec, req) if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Log in") { - t.Errorf("host %q /login: got %d", host, rec.Code) + t.Errorf("host %q /webadmin: got %d", host, rec.Code) } } } @@ -48,7 +48,7 @@ func TestHostRoutingWithoutDB(t *testing.T) { func TestRootRoutePrecedence(t *testing.T) { cfg := &config.Config{BaseDomain: "example.com", JWTSecret: []byte("x"), MaxUploadBytes: 1 << 20} s := NewServer(cfg, nil) - for path, want := range map[string]int{"/login": 200, "/healthz": 200, "/dashboard": 303, "/admin/": 303, "/b/alice/posts": 303, "/account/password": 303} { + for path, want := range map[string]int{"/webadmin": 200, "/healthz": 200, "/dashboard": 303, "/admin/": 303, "/b/alice/posts": 303, "/account/password": 303} { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", path, nil) req.Host = "example.com" @@ -57,7 +57,7 @@ func TestRootRoutePrecedence(t *testing.T) { t.Errorf("%s: got %d, want %d", path, rec.Code, want) } } - for _, slug := range []string{"login", "admin", "b", "media", "feed.xml"} { + for _, slug := range []string{"webadmin", "admin", "b", "media", "feed.xml"} { if !reservedPageSlugs[slug] { t.Errorf("page slug %q should be reserved", slug) } @@ -91,3 +91,16 @@ func TestAllTemplatesParse(t *testing.T) { } } } + +// /webadmin on a blog's own host bounces to the root login page and back to that blog's dashboard. +func TestSubdomainWebadminRedirect(t *testing.T) { + cfg := &config.Config{BaseDomain: "example.com", JWTSecret: []byte("x"), MaxUploadBytes: 1 << 20} + s := NewServer(cfg, nil) + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/webadmin", nil) + req.Host = "alice.example.com" + s.ServeHTTP(rec, req) + if want := "http://example.com/webadmin?next=%2Fb%2Falice%2F"; rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != want { + t.Errorf("got %d %q, want 303 %q", rec.Code, rec.Header().Get("Location"), want) + } +} |
