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}