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 // Users (authenticated).
53 mux.HandleFunc("POST /api/users/me/avatar", s.requireAuth(h.HandleUploadAvatar))
54 mux.HandleFunc("GET /api/users/{id}/avatar", s.requireAuth(h.HandleServeAvatar))
55
56 // SPA — serve index.html for all non-API routes (client-side routing).
57 mux.Handle("/", s.spaHandler(webFS))
58
59 return s, nil
60}
61
62type Server struct {
63 cfg config.Config
64 mux *http.ServeMux
65 auth *auth.SessionManager
66}
67
68func (s *Server) Start(ctx context.Context) error {
69 addr := s.cfg.Server.Host + ":" + itoa(s.cfg.Server.Port)
70 srv := &http.Server{Addr: addr, Handler: s.mux}
71
72 go func() {
73 <-ctx.Done()
74 srv.Shutdown(context.Background())
75 }()
76
77 return srv.ListenAndServe()
78}
79
80func itoa(n int) string {
81 if n == 0 {
82 return "0"
83 }
84 var buf [20]byte
85 i := len(buf)
86 for n > 0 {
87 i--
88 buf[i] = byte('0' + n%10)
89 n /= 10
90 }
91 return string(buf[i:])
92}
93
94// spaHandler serves static files for SPA client-side routing.
95// For routes that don't match an existing file (e.g. /browse, /image/123),
96// it falls back to serving index.html so the SPA router can handle them.
97func (s *Server) spaHandler(webFS fs.FS) http.HandlerFunc {
98 fsHandler := http.FileServer(http.FS(webFS))
99 return func(w http.ResponseWriter, r *http.Request) {
100 // API routes should not be served by the SPA handler.
101 if strings.HasPrefix(r.URL.Path, "/api/") {
102 http.NotFound(w, r)
103 return
104 }
105
106 // Check if the requested file exists in the embedded FS.
107 path := strings.TrimPrefix(r.URL.Path, "/")
108 if _, err := fs.Stat(webFS, path); err != nil {
109 // File doesn't exist — serve index.html for SPA routing.
110 r.URL.Path = "/"
111 fsHandler.ServeHTTP(w, r)
112 return
113 }
114
115 // File exists — serve it directly.
116 fsHandler.ServeHTTP(w, r)
117 }
118}
119
120func handleHealth(w http.ResponseWriter, r *http.Request) {
121 w.Header().Set("Content-Type", "application/json")
122 w.Write([]byte(`{"status":"ok"}`))
123}