aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/routes.go
diff options
context:
space:
mode:
authorgramanas <grm@eyesin.space>2026-09-12 11:24:17 +0300
committergramanas <grm@eyesin.space>2026-09-12 11:24:17 +0300
commit3eb04b1a2bdf9e53231fe862cfd76327371a9741 (patch)
treeb38b2d82a47233fd8e0bb18c59e4a8f3dd2412d7 /internal/web/routes.go
downloadblogspace-3eb04b1a2bdf9e53231fe862cfd76327371a9741.tar.gz
blogspace-3eb04b1a2bdf9e53231fe862cfd76327371a9741.tar.bz2
blogspace-3eb04b1a2bdf9e53231fe862cfd76327371a9741.zip
Initial multi-tenant blog host
Go + Postgres application serving a management dashboard on the base domain and one public blog per subdomain. Markdown posts organised in pages, form-based theme customisation, image uploads stored in Postgres, JWT cookie sessions with CSRF, superadmin user management, RSS feeds. Docker/compose deployment and a Makefile-driven dev environment with seed data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web/routes.go')
-rw-r--r--internal/web/routes.go81
1 files changed, 81 insertions, 0 deletions
diff --git a/internal/web/routes.go b/internal/web/routes.go
new file mode 100644
index 0000000..c889aa1
--- /dev/null
+++ b/internal/web/routes.go
@@ -0,0 +1,81 @@
+package web
+
+import (
+ "net/http"
+ "net/url"
+)
+
+func urlQuery(s string) string { return url.QueryEscape(s) }
+
+func (s *Server) rootRoutes() http.Handler {
+ m := http.NewServeMux()
+ m.HandleFunc("GET /{$}", s.handleIndex)
+ 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("POST /logout", s.handleLogout)
+ m.HandleFunc("GET /dashboard", s.requireAuth(s.handleDashboard))
+ m.HandleFunc("GET /account/password", s.requireAuth(s.handlePasswordForm))
+ m.HandleFunc("POST /account/password", s.requireAuth(s.handlePassword))
+
+ // blog management (owner or superadmin)
+ m.HandleFunc("GET /b/{sub}/{$}", s.withBlog(s.handleBlogOverview))
+ m.HandleFunc("GET /b/{sub}/settings", s.withBlog(s.handleSettingsForm))
+ m.HandleFunc("POST /b/{sub}/settings", s.withBlog(s.handleSettings))
+ m.HandleFunc("GET /b/{sub}/pages", s.withBlog(s.handlePages))
+ m.HandleFunc("GET /b/{sub}/pages/new", s.withBlog(s.handlePageForm))
+ m.HandleFunc("POST /b/{sub}/pages/new", s.withBlog(s.handlePageSave))
+ m.HandleFunc("GET /b/{sub}/pages/{id}/edit", s.withBlog(s.handlePageForm))
+ m.HandleFunc("POST /b/{sub}/pages/{id}/edit", s.withBlog(s.handlePageSave))
+ m.HandleFunc("GET /b/{sub}/pages/{id}/delete", s.withBlog(s.handlePageDeleteConfirm))
+ m.HandleFunc("POST /b/{sub}/pages/{id}/delete", s.withBlog(s.handlePageDelete))
+ m.HandleFunc("POST /b/{sub}/pages/{id}/move", s.withBlog(s.handlePageMove))
+ m.HandleFunc("POST /b/{sub}/pages/{id}/home", s.withBlog(s.handlePageHome))
+ m.HandleFunc("GET /b/{sub}/posts", s.withBlog(s.handlePosts))
+ m.HandleFunc("GET /b/{sub}/posts/new", s.withBlog(s.handlePostForm))
+ m.HandleFunc("POST /b/{sub}/posts/new", s.withBlog(s.handlePostSave))
+ m.HandleFunc("GET /b/{sub}/posts/{id}/edit", s.withBlog(s.handlePostForm))
+ m.HandleFunc("POST /b/{sub}/posts/{id}/edit", s.withBlog(s.handlePostSave))
+ m.HandleFunc("GET /b/{sub}/posts/{id}/delete", s.withBlog(s.handlePostDeleteConfirm))
+ m.HandleFunc("POST /b/{sub}/posts/{id}/delete", s.withBlog(s.handlePostDelete))
+ m.HandleFunc("GET /b/{sub}/design", s.withBlog(s.handleDesignForm))
+ m.HandleFunc("POST /b/{sub}/design", s.withBlog(s.handleDesign))
+ m.HandleFunc("GET /b/{sub}/images", s.withBlog(s.handleImages))
+ m.HandleFunc("POST /b/{sub}/images/upload", s.withBlog(s.handleImageUpload))
+ m.HandleFunc("POST /b/{sub}/images/{id}/delete", s.withBlog(s.handleImageDelete))
+
+ // superadmin
+ m.HandleFunc("GET /admin/{$}", s.requireAdmin(s.handleAdmin))
+ m.HandleFunc("GET /admin/users/new", s.requireAdmin(s.handleAdminNewUserForm))
+ m.HandleFunc("POST /admin/users/new", s.requireAdmin(s.handleAdminNewUser))
+ m.HandleFunc("POST /admin/users/{id}/reset-password", s.requireAdmin(s.handleAdminResetPassword))
+ m.HandleFunc("POST /admin/users/{id}/disable", s.requireAdmin(s.handleAdminSetDisabled(true)))
+ m.HandleFunc("POST /admin/users/{id}/enable", s.requireAdmin(s.handleAdminSetDisabled(false)))
+ m.HandleFunc("GET /admin/users/{id}/delete", s.requireAdmin(s.handleAdminDeleteUserConfirm))
+ m.HandleFunc("POST /admin/users/{id}/delete", s.requireAdmin(s.handleAdminDeleteUser))
+
+ m.HandleFunc("GET /media/{id}", s.handleMedia)
+ m.Handle("GET /static/{file}", s.staticHandler())
+ return s.session(m)
+}
+
+func (s *Server) blogRoutes() http.Handler {
+ m := http.NewServeMux()
+ m.HandleFunc("GET /{$}", s.handleBlogHome)
+ m.HandleFunc("GET /feed.xml", s.handleBlogFeed)
+ m.HandleFunc("GET /media/{id}", s.handleMedia)
+ m.Handle("GET /static/{file}", s.staticHandler())
+ m.HandleFunc("GET /{page}", s.handleBlogPage)
+ m.HandleFunc("GET /{page}/{post}", s.handleBlogPost)
+ return m
+}
+
+func (s *Server) staticHandler() http.Handler {
+ fs := http.StripPrefix("/static/", http.FileServer(http.FS(s.tpl.AssetsFS())))
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !s.cfg.Dev {
+ w.Header().Set("Cache-Control", "public, max-age=3600")
+ }
+ fs.ServeHTTP(w, r)
+ })
+}