package server import ( "context" "io/fs" "net/http" "strings" "github.com/mjensen/weimar/internal/api" "github.com/mjensen/weimar/internal/auth" "github.com/mjensen/weimar/internal/config" "github.com/mjensen/weimar/internal/db" "github.com/mjensen/weimar/internal/storage" ) func New(cfg config.Config, webFS fs.FS, database *db.DB) (*Server, error) { sm := auth.NewSessionManager(database) st, err := storage.New(cfg.Storage.Path) if err != nil { return nil, err } h := api.New(database, sm, st, cfg) mux := http.NewServeMux() // Health check (public). mux.HandleFunc("GET /api/health", handleHealth) s := &Server{ cfg: cfg, mux: mux, auth: sm, } // Auth (public). mux.HandleFunc("POST /api/auth/login", h.HandleLogin) mux.HandleFunc("POST /api/auth/register", h.HandleRegister) // Auth (authenticated). mux.HandleFunc("POST /api/auth/logout", s.requireAuth(h.HandleLogout)) // Images (authenticated). mux.HandleFunc("POST /api/upload", s.requireAuth(h.HandleUpload)) mux.HandleFunc("GET /api/images", s.requireAuth(h.HandleListImages)) mux.HandleFunc("GET /api/images/{id}", s.requireAuth(h.HandleGetImage)) mux.HandleFunc("GET /api/images/{id}/download", s.requireAuth(h.HandleDownload)) mux.HandleFunc("GET /api/images/{id}/thumbnail", s.requireAuth(h.HandleThumbnail)) // Tags (authenticated). mux.HandleFunc("GET /api/tags/suggest", s.requireAuth(h.HandleTagSuggest)) // Users (authenticated). mux.HandleFunc("POST /api/users/me/avatar", s.requireAuth(h.HandleUploadAvatar)) mux.HandleFunc("GET /api/users/{id}/avatar", s.requireAuth(h.HandleServeAvatar)) // SPA — serve index.html for all non-API routes (client-side routing). mux.Handle("/", s.spaHandler(webFS)) return s, nil } type Server struct { cfg config.Config mux *http.ServeMux auth *auth.SessionManager } func (s *Server) Start(ctx context.Context) error { addr := s.cfg.Server.Host + ":" + itoa(s.cfg.Server.Port) srv := &http.Server{Addr: addr, Handler: s.mux} go func() { <-ctx.Done() srv.Shutdown(context.Background()) }() return srv.ListenAndServe() } func itoa(n int) string { if n == 0 { return "0" } var buf [20]byte i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } return string(buf[i:]) } // spaHandler serves static files for SPA client-side routing. // For routes that don't match an existing file (e.g. /browse, /image/123), // it falls back to serving index.html so the SPA router can handle them. func (s *Server) spaHandler(webFS fs.FS) http.HandlerFunc { fsHandler := http.FileServer(http.FS(webFS)) return func(w http.ResponseWriter, r *http.Request) { // API routes should not be served by the SPA handler. if strings.HasPrefix(r.URL.Path, "/api/") { http.NotFound(w, r) return } // Check if the requested file exists in the embedded FS. path := strings.TrimPrefix(r.URL.Path, "/") if _, err := fs.Stat(webFS, path); err != nil { // File doesn't exist — serve index.html for SPA routing. r.URL.Path = "/" fsHandler.ServeHTTP(w, r) return } // File exists — serve it directly. fsHandler.ServeHTTP(w, r) } } func handleHealth(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) }