all repos — weimar @ 74fc3e1e24b47e370bcf5ea6b4e016cb50630920

Unnamed repository; edit this file 'description' to name the repository.

internal/server/server.go (view raw)

  1package server
  2
  3import (
  4	"context"
  5	"io/fs"
  6	"net/http"
  7	"strings"
  8
  9	"github.com/mjensen/weimar/internal/api"
 10	"github.com/mjensen/weimar/internal/auth"
 11	"github.com/mjensen/weimar/internal/config"
 12	"github.com/mjensen/weimar/internal/db"
 13	"github.com/mjensen/weimar/internal/storage"
 14)
 15
 16func New(cfg config.Config, webFS fs.FS, database *db.DB) (*Server, error) {
 17	sm := auth.NewSessionManager(database)
 18	st, err := storage.New(cfg.Storage.Path)
 19	if err != nil {
 20		return nil, err
 21	}
 22	h := api.New(database, sm, st, cfg)
 23
 24	mux := http.NewServeMux()
 25
 26	// Health check (public).
 27	mux.HandleFunc("GET /api/health", handleHealth)
 28
 29	s := &Server{
 30		cfg:  cfg,
 31		mux:  mux,
 32		auth: sm,
 33	}
 34
 35	// Auth (public).
 36	mux.HandleFunc("POST /api/auth/login", h.HandleLogin)
 37	mux.HandleFunc("POST /api/auth/register", h.HandleRegister)
 38
 39	// Auth (authenticated).
 40	mux.HandleFunc("POST /api/auth/logout", s.requireAuth(h.HandleLogout))
 41
 42	// Images (authenticated).
 43	mux.HandleFunc("POST /api/upload", s.requireAuth(h.HandleUpload))
 44	mux.HandleFunc("GET /api/images", s.requireAuth(h.HandleListImages))
 45	mux.HandleFunc("GET /api/images/{id}", s.requireAuth(h.HandleGetImage))
 46	mux.HandleFunc("GET /api/images/{id}/download", s.requireAuth(h.HandleDownload))
 47	mux.HandleFunc("GET /api/images/{id}/thumbnail", s.requireAuth(h.HandleThumbnail))
 48
 49	// Tags (authenticated).
 50	mux.HandleFunc("GET /api/tags/suggest", s.requireAuth(h.HandleTagSuggest))
 51
 52	// SPA — serve index.html for all non-API routes (client-side routing).
 53	mux.Handle("/", s.spaHandler(webFS))
 54
 55	return s, nil
 56}
 57
 58type Server struct {
 59	cfg  config.Config
 60	mux  *http.ServeMux
 61	auth *auth.SessionManager
 62}
 63
 64func (s *Server) Start(ctx context.Context) error {
 65	addr := s.cfg.Server.Host + ":" + itoa(s.cfg.Server.Port)
 66	srv := &http.Server{Addr: addr, Handler: s.mux}
 67
 68	go func() {
 69		<-ctx.Done()
 70		srv.Shutdown(context.Background())
 71	}()
 72
 73	return srv.ListenAndServe()
 74}
 75
 76func itoa(n int) string {
 77	if n == 0 {
 78		return "0"
 79	}
 80	var buf [20]byte
 81	i := len(buf)
 82	for n > 0 {
 83		i--
 84		buf[i] = byte('0' + n%10)
 85		n /= 10
 86	}
 87	return string(buf[i:])
 88}
 89
 90// spaHandler serves static files for SPA client-side routing.
 91// For routes that don't match an existing file (e.g. /browse, /image/123),
 92// it falls back to serving index.html so the SPA router can handle them.
 93func (s *Server) spaHandler(webFS fs.FS) http.HandlerFunc {
 94	fsHandler := http.FileServer(http.FS(webFS))
 95	return func(w http.ResponseWriter, r *http.Request) {
 96		// API routes should not be served by the SPA handler.
 97		if strings.HasPrefix(r.URL.Path, "/api/") {
 98			http.NotFound(w, r)
 99			return
100		}
101
102		// Check if the requested file exists in the embedded FS.
103		path := strings.TrimPrefix(r.URL.Path, "/")
104		if _, err := fs.Stat(webFS, path); err != nil {
105			// File doesn't exist — serve index.html for SPA routing.
106			r.URL.Path = "/"
107			fsHandler.ServeHTTP(w, r)
108			return
109		}
110
111		// File exists — serve it directly.
112		fsHandler.ServeHTTP(w, r)
113	}
114}
115
116func handleHealth(w http.ResponseWriter, r *http.Request) {
117	w.Header().Set("Content-Type", "application/json")
118	w.Write([]byte(`{"status":"ok"}`))
119}