summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/server.go186
1 files changed, 109 insertions, 77 deletions
diff --git a/internal/api/server.go b/internal/api/server.go
index bcc8330..23cb5eb 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -107,102 +107,134 @@ func NewServerWithLogger(
return s
}
-func (s *Server) routes() {
- // Public routes — use plain path so wrong method returns 405 instead of falling through to /
- s.mux.HandleFunc("/api/bootstrap", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
- return
- }
- s.handleBootstrap(w, r)
- })
- s.mux.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
- return
- }
- s.handleLogin(w, r)
- })
- s.mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
- return
- }
- s.handleHealthz(w, r)
- })
- s.mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
+// requireSession wraps a handler with the session requirement middleware.
+func (s *Server) requireSession(h http.HandlerFunc) http.HandlerFunc {
+ return s.mw.RequireSession(h).(http.HandlerFunc)
+}
+
+// requireAdmin wraps a handler with both session and admin middleware.
+func (s *Server) requireAdmin(h http.HandlerFunc) http.HandlerFunc {
+ return s.mw.RequireSession(s.mw.RequireAdmin(h)).(http.HandlerFunc)
+}
+
+// publicMethod wraps a handler so only the given HTTP method is allowed;
+// other methods yield 405 instead of falling through.
+func publicMethod(method string, handler http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != method {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
- s.handleReadyz(w, r)
- })
+ handler(w, r)
+ }
+}
+
+// routesPublic wires the fully-public API endpoints (bootstrap, login, probes).
+func (s *Server) routesPublic() {
+ s.mux.HandleFunc("/api/bootstrap", publicMethod(http.MethodPost, s.handleBootstrap))
+ s.mux.HandleFunc("/api/login", publicMethod(http.MethodPost, s.handleLogin))
+ s.mux.HandleFunc("/healthz", publicMethod(http.MethodGet, s.handleHealthz))
+ s.mux.HandleFunc("/readyz", publicMethod(http.MethodGet, s.handleReadyz))
+}
- // Public share routes
+// routesSharePublic wires public share routes (no session required).
+func (s *Server) routesSharePublic() {
s.mux.HandleFunc("GET /s/{token}", s.handleSharePage)
s.mux.HandleFunc("GET /s/{token}/stream", s.handleShareStream)
s.mux.HandleFunc("GET /s/{token}/thumbnail", s.handleShareThumbnail)
s.mux.HandleFunc("GET /s/{token}/download", s.handleShareDownload)
+}
- // Static assets (public)
+// routesStatic wires static CSS/JS asset serving.
+func (s *Server) routesStatic() {
staticHandler := http.FileServer(s.staticFS)
s.mux.Handle("/css/", staticHandler)
s.mux.Handle("/js/", staticHandler)
+}
- // HTML pages
+// routesHTML wires the SPA HTML page routes.
+func (s *Server) routesHTML() {
s.mux.Handle("/login.html", http.HandlerFunc(s.serveLogin))
s.mux.Handle("/bootstrap.html", http.HandlerFunc(s.serveBootstrap))
s.mux.Handle("/", s.mw.RequireSession(http.HandlerFunc(s.serveIndex)))
s.mux.Handle("GET /index.html", s.mw.RequireSession(http.HandlerFunc(s.serveIndex)))
s.mux.Handle("GET /detach.html", s.mw.RequireSession(http.HandlerFunc(s.serveDetach)))
+}
+
+// routesAuth wires the logout route.
+func (s *Server) routesAuth() {
+ s.mux.Handle("POST /api/logout", s.requireSession(s.handleLogout))
+}
+
+// routesSets wires the set-related API routes.
+func (s *Server) routesSets() {
+ s.mux.Handle("GET /api/sets", s.requireSession(s.handleListSets))
+ s.mux.Handle("GET /api/sets/{id}/browse", s.requireSession(s.handleBrowseSet))
+ s.mux.Handle("GET /api/sets/{id}/cover", s.requireSession(s.handleGetSetCover))
+ s.mux.Handle("POST /api/sets/{id}/cover", s.requireSession(s.handlePostSetCover))
+ s.mux.Handle("POST /api/sets/{id}/upload", s.requireSession(s.handleUpload))
+}
+
+// routesMedia wires the media-related API routes.
+func (s *Server) routesMedia() {
+ s.mux.Handle("GET /api/media", s.requireSession(s.handleListMedia))
+ s.mux.Handle("GET /api/media/{id}", s.requireSession(s.handleGetMedia))
+ s.mux.Handle("GET /api/media/{id}/stream", s.requireSession(s.handleStream))
+ s.mux.Handle("GET /api/media/{id}/download", s.requireSession(s.handleDownload))
+ s.mux.Handle("GET /api/media/{id}/thumbnail", s.requireSession(s.handleThumbnail))
+ s.mux.Handle("POST /api/media/{id}/thumbnail", s.requireSession(s.handleRegenThumbnail))
+ s.mux.Handle("POST /api/media/{id}/favorite", s.requireSession(s.handleFavorite))
+ s.mux.Handle("POST /api/media/{id}/tags", s.requireSession(s.handleAddTag))
+ s.mux.Handle("DELETE /api/media/{id}/tags/{tag}", s.requireSession(s.handleRemoveTag))
+ s.mux.Handle("DELETE /api/media/{id}", s.requireSession(s.handleSoftDelete))
+ s.mux.Handle("POST /api/media/{id}/restore", s.requireSession(s.handleRestore))
+ s.mux.Handle("POST /api/media/{id}/shares", s.requireSession(s.handleCreateShare))
+ s.mux.Handle("GET /api/media/{id}/shares", s.requireSession(s.handleListShares))
+}
- // Session-required routes
- s.mux.Handle("POST /api/logout", s.mw.RequireSession(http.HandlerFunc(s.handleLogout)))
-
- // Sets
- s.mux.Handle("GET /api/sets", s.mw.RequireSession(http.HandlerFunc(s.handleListSets)))
- s.mux.Handle("GET /api/sets/{id}/browse", s.mw.RequireSession(http.HandlerFunc(s.handleBrowseSet)))
- s.mux.Handle("GET /api/sets/{id}/cover", s.mw.RequireSession(http.HandlerFunc(s.handleGetSetCover)))
- s.mux.Handle("POST /api/sets/{id}/cover", s.mw.RequireSession(http.HandlerFunc(s.handlePostSetCover)))
- s.mux.Handle("POST /api/sets/{id}/upload", s.mw.RequireSession(http.HandlerFunc(s.handleUpload)))
-
- // Media
- s.mux.Handle("GET /api/media", s.mw.RequireSession(http.HandlerFunc(s.handleListMedia)))
- s.mux.Handle("GET /api/media/{id}", s.mw.RequireSession(http.HandlerFunc(s.handleGetMedia)))
- s.mux.Handle("GET /api/media/{id}/stream", s.mw.RequireSession(http.HandlerFunc(s.handleStream)))
- s.mux.Handle("GET /api/media/{id}/download", s.mw.RequireSession(http.HandlerFunc(s.handleDownload)))
- s.mux.Handle("GET /api/media/{id}/thumbnail", s.mw.RequireSession(http.HandlerFunc(s.handleThumbnail)))
- s.mux.Handle("POST /api/media/{id}/thumbnail", s.mw.RequireSession(http.HandlerFunc(s.handleRegenThumbnail)))
- s.mux.Handle("POST /api/media/{id}/favorite", s.mw.RequireSession(http.HandlerFunc(s.handleFavorite)))
- s.mux.Handle("POST /api/media/{id}/tags", s.mw.RequireSession(http.HandlerFunc(s.handleAddTag)))
- s.mux.Handle("DELETE /api/media/{id}/tags/{tag}", s.mw.RequireSession(http.HandlerFunc(s.handleRemoveTag)))
- s.mux.Handle("DELETE /api/media/{id}", s.mw.RequireSession(http.HandlerFunc(s.handleSoftDelete)))
- s.mux.Handle("POST /api/media/{id}/restore", s.mw.RequireSession(http.HandlerFunc(s.handleRestore)))
- s.mux.Handle("POST /api/media/{id}/shares", s.mw.RequireSession(http.HandlerFunc(s.handleCreateShare)))
- s.mux.Handle("GET /api/media/{id}/shares", s.mw.RequireSession(http.HandlerFunc(s.handleListShares)))
-
- // Notes
- s.mux.Handle("GET /api/media/{id}/notes", s.mw.RequireSession(http.HandlerFunc(s.handleGetNote)))
- s.mux.Handle("POST /api/media/{id}/notes", s.mw.RequireSession(http.HandlerFunc(s.handleUpsertNote)))
- s.mux.Handle("DELETE /api/media/{id}/notes", s.mw.RequireSession(http.HandlerFunc(s.handleDeleteNote)))
-
- // Progress
- s.mux.Handle("POST /api/progress", s.mw.RequireSession(http.HandlerFunc(s.handleProgress)))
-
- // Shares
- s.mux.Handle("DELETE /api/shares/{token}", s.mw.RequireSession(http.HandlerFunc(s.handleRevokeShare)))
- s.mux.Handle("GET /api/shares", s.mw.RequireSession(http.HandlerFunc(s.handleMyShares)))
-
- // Admin routes
- s.mux.Handle("GET /api/admin/trash", s.mw.RequireSession(s.mw.RequireAdmin(http.HandlerFunc(s.handleListTrash))))
- s.mux.Handle("POST /api/admin/rescan", s.mw.RequireSession(s.mw.RequireAdmin(http.HandlerFunc(s.handleRescan))))
- s.mux.Handle("GET /api/admin/scan-progress", s.mw.RequireSession(s.mw.RequireAdmin(http.HandlerFunc(s.handleScanProgress))))
- s.mux.Handle("GET /api/admin/users", s.mw.RequireSession(s.mw.RequireAdmin(http.HandlerFunc(s.handleListUsers))))
- s.mux.Handle("POST /api/admin/users", s.mw.RequireSession(s.mw.RequireAdmin(http.HandlerFunc(s.handleCreateUser))))
- s.mux.Handle("DELETE /api/admin/users/{id}", s.mw.RequireSession(s.mw.RequireAdmin(http.HandlerFunc(s.handleDeleteUser))))
- s.mux.Handle("GET /api/admin/permissions", s.mw.RequireSession(s.mw.RequireAdmin(http.HandlerFunc(s.handleListPermissions))))
- s.mux.Handle("POST /api/admin/permissions", s.mw.RequireSession(s.mw.RequireAdmin(http.HandlerFunc(s.handleGrantPermission))))
- s.mux.Handle("DELETE /api/admin/permissions", s.mw.RequireSession(s.mw.RequireAdmin(http.HandlerFunc(s.handleRevokePermission))))
+// routesNotes wires the notes API routes.
+func (s *Server) routesNotes() {
+ s.mux.Handle("GET /api/media/{id}/notes", s.requireSession(s.handleGetNote))
+ s.mux.Handle("POST /api/media/{id}/notes", s.requireSession(s.handleUpsertNote))
+ s.mux.Handle("DELETE /api/media/{id}/notes", s.requireSession(s.handleDeleteNote))
+}
+
+// routesProgress wires the progress API routes.
+func (s *Server) routesProgress() {
+ s.mux.Handle("POST /api/progress", s.requireSession(s.handleProgress))
+}
+
+// routesShares wires the share-management API routes.
+func (s *Server) routesShares() {
+ s.mux.Handle("DELETE /api/shares/{token}", s.requireSession(s.handleRevokeShare))
+ s.mux.Handle("GET /api/shares", s.requireSession(s.handleMyShares))
+}
+
+// routesAdmin wires the admin-only API routes.
+func (s *Server) routesAdmin() {
+ s.mux.Handle("GET /api/admin/trash", s.requireAdmin(s.handleListTrash))
+ s.mux.Handle("POST /api/admin/rescan", s.requireAdmin(s.handleRescan))
+ s.mux.Handle("GET /api/admin/scan-progress", s.requireAdmin(s.handleScanProgress))
+ s.mux.Handle("GET /api/admin/users", s.requireAdmin(s.handleListUsers))
+ s.mux.Handle("POST /api/admin/users", s.requireAdmin(s.handleCreateUser))
+ s.mux.Handle("DELETE /api/admin/users/{id}", s.requireAdmin(s.handleDeleteUser))
+ s.mux.Handle("GET /api/admin/permissions", s.requireAdmin(s.handleListPermissions))
+ s.mux.Handle("POST /api/admin/permissions", s.requireAdmin(s.handleGrantPermission))
+ s.mux.Handle("DELETE /api/admin/permissions", s.requireAdmin(s.handleRevokePermission))
+}
+
+func (s *Server) routes() {
+ s.routesPublic()
+ s.routesSharePublic()
+ s.routesStatic()
+ s.routesHTML()
+ s.routesAuth()
+ s.routesSets()
+ s.routesMedia()
+ s.routesNotes()
+ s.routesProgress()
+ s.routesShares()
+ s.routesAdmin()
}
func (s *Server) pingStore(ctx context.Context) error {