From caa2247b89d19e5c021e671043df08e49169598d Mon Sep 17 00:00:00 2001 From: mkelvers Date: Mon, 6 Apr 2026 22:32:22 +0200 Subject: [PATCH] refactor: simplify main.go and introduce central router with modular handlers --- cmd/server/main.go | 168 +++---------------------------------- internal/handlers/anime.go | 137 ++++++++++++++++++++++++++++++ internal/server/routes.go | 58 +++++++++++++ 3 files changed, 208 insertions(+), 155 deletions(-) create mode 100644 internal/handlers/anime.go create mode 100644 internal/server/routes.go diff --git a/cmd/server/main.go b/cmd/server/main.go index e09b0f8..ba33a9a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -5,16 +5,13 @@ import ( "log" "net/http" "os" - "strconv" _ "github.com/mattn/go-sqlite3" "malago/internal/auth" "malago/internal/database" - "malago/internal/handlers" "malago/internal/jikan" - "malago/internal/middleware" - "malago/internal/templates" + "malago/internal/server" ) func main() { @@ -36,162 +33,23 @@ func main() { queries := database.New(db) authService := auth.NewService(queries) - authHandler := handlers.NewAuthHandler(authService) - watchlistHandler := handlers.NewWatchlistHandler(queries) - jikanClient := jikan.NewClient() - mux := http.NewServeMux() + app := server.Config{ + DB: queries, + JikanClient: jikanClient, + AuthService: authService, + } - // Serve static files - fs := http.FileServer(http.Dir("./static")) - mux.Handle("/static/", http.StripPrefix("/static/", fs)) + handler := server.NewRouter(app) - // Homepage (Catalog) - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - templates.Catalog().Render(r.Context(), w) - }) + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } - // Search page - mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query().Get("q") - if query == "" { - templates.Search("").Render(r.Context(), w) - return - } - - // Check if HTMX request for results only - if r.Header.Get("HX-Request") == "true" { - res, err := jikanClient.Search(query, 1) - if err != nil { - log.Printf("search error: %v", err) - http.Error(w, "Failed to search anime", http.StatusInternalServerError) - return - } - templates.SearchResultsWrapper(query, res.Animes, 2, res.HasNextPage).Render(r.Context(), w) - return - } - - // Full page with query - templates.Search(query).Render(r.Context(), w) - }) - - // Search endpoint (HTMX Infinite Scroll) - mux.HandleFunc("/api/search", func(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query().Get("q") - pageStr := r.URL.Query().Get("page") - page, _ := strconv.Atoi(pageStr) - if page < 1 { - page = 1 - } - - res, err := jikanClient.Search(query, page) - if err != nil { - log.Printf("search pagination error: %v", err) - http.Error(w, "Failed to fetch search page", http.StatusInternalServerError) - return - } - - templates.SearchItems(query, res.Animes, page+1, res.HasNextPage).Render(r.Context(), w) - }) - - // Catalog endpoint (HTMX Infinite Scroll) - mux.HandleFunc("/api/catalog", func(w http.ResponseWriter, r *http.Request) { - pageStr := r.URL.Query().Get("page") - page, _ := strconv.Atoi(pageStr) - if page < 1 { - page = 1 - } - - res, err := jikanClient.GetTopAnime(page) - if err != nil { - log.Printf("top anime error: %v", err) - http.Error(w, "Failed to fetch top anime", http.StatusInternalServerError) - return - } - - templates.CatalogItems(res.Animes, page+1, res.HasNextPage).Render(r.Context(), w) - }) - - // Anime Details page - mux.HandleFunc("/anime/", func(w http.ResponseWriter, r *http.Request) { - idStr := r.URL.Path[len("/anime/"):] - id, err := strconv.Atoi(idStr) - if err != nil || id <= 0 { - http.NotFound(w, r) - return - } - - anime, err := jikanClient.GetAnimeByID(id) - if err != nil { - log.Printf("anime fetch error for %d: %v", id, err) - http.Error(w, "Failed to fetch anime details", http.StatusInternalServerError) - return - } - - // Get current watchlist status if user is logged in - currentStatus := "" - if user := middleware.GetUser(r.Context()); user != nil { - entry, err := queries.GetWatchListEntry(r.Context(), database.GetWatchListEntryParams{ - UserID: user.ID, - AnimeID: int64(id), - }) - if err == nil { - currentStatus = entry.Status - } - } - - templates.AnimeDetails(anime, currentStatus).Render(r.Context(), w) - }) - - // Anime Relations API endpoint (HTMX "Suspense") - mux.HandleFunc("/api/anime/", func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path[len("/api/anime/"):] - idStr := "" - for i, c := range path { - if c == '/' { - idStr = path[:i] - break - } - } - - id, _ := strconv.Atoi(idStr) - if id <= 0 { - http.Error(w, "invalid id", http.StatusBadRequest) - return - } - - relations := jikanClient.GetFullRelations(id) - templates.AnimeRelationsList(relations).Render(r.Context(), w) - }) - - // Auth Endpoints - mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - authHandler.HandleLoginPage(w, r) - } else { - authHandler.HandleLogin(w, r) - } - }) - mux.HandleFunc("/logout", authHandler.HandleLogout) - - // Watchlist POST endpoint (Protected) - mux.Handle("/api/watchlist/export", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleExportWatchlist))) - mux.Handle("/api/watchlist/import", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleImportWatchlist))) - mux.Handle("/api/watchlist", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleUpdateWatchlist))) - mux.Handle("/api/watchlist/", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleDeleteWatchlist))) - mux.Handle("/watchlist", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleGetWatchlist))) - - // Wrap mux with global auth checking, THEN auth context parsing - protectedHandler := middleware.RequireGlobalAuth(mux) - handler := middleware.Auth(authService)(protectedHandler) - - log.Println("Server starting on http://localhost:3000") - if err := http.ListenAndServe(":3000", handler); err != nil { + log.Printf("Server starting on http://localhost:%s", port) + if err := http.ListenAndServe(":"+port, handler); err != nil { log.Fatalf("Server failed to start: %v", err) } } diff --git a/internal/handlers/anime.go b/internal/handlers/anime.go new file mode 100644 index 0000000..a7b42a8 --- /dev/null +++ b/internal/handlers/anime.go @@ -0,0 +1,137 @@ +package handlers + +import ( + "log" + "net/http" + "strconv" + + "malago/internal/database" + "malago/internal/jikan" + "malago/internal/middleware" + "malago/internal/templates" +) + +type AnimeHandler struct { + jikan *jikan.Client + db *database.Queries +} + +func NewAnimeHandler(jikan *jikan.Client, db *database.Queries) *AnimeHandler { + return &AnimeHandler{jikan: jikan, db: db} +} + +func (h *AnimeHandler) HandleCatalog(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + templates.Catalog().Render(r.Context(), w) +} + +func (h *AnimeHandler) HandleSearch(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + if query == "" { + templates.Search("").Render(r.Context(), w) + return + } + + // Check if HTMX request for results only + if r.Header.Get("HX-Request") == "true" { + res, err := h.jikan.Search(query, 1) + if err != nil { + log.Printf("search error: %v", err) + http.Error(w, "Failed to search anime", http.StatusInternalServerError) + return + } + templates.SearchResultsWrapper(query, res.Animes, 2, res.HasNextPage).Render(r.Context(), w) + return + } + + // Full page with query + templates.Search(query).Render(r.Context(), w) +} + +func (h *AnimeHandler) HandleAPISearch(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + pageStr := r.URL.Query().Get("page") + page, _ := strconv.Atoi(pageStr) + if page < 1 { + page = 1 + } + + res, err := h.jikan.Search(query, page) + if err != nil { + log.Printf("search pagination error: %v", err) + http.Error(w, "Failed to fetch search page", http.StatusInternalServerError) + return + } + + templates.SearchItems(query, res.Animes, page+1, res.HasNextPage).Render(r.Context(), w) +} + +func (h *AnimeHandler) HandleAPICatalog(w http.ResponseWriter, r *http.Request) { + pageStr := r.URL.Query().Get("page") + page, _ := strconv.Atoi(pageStr) + if page < 1 { + page = 1 + } + + res, err := h.jikan.GetTopAnime(page) + if err != nil { + log.Printf("top anime error: %v", err) + http.Error(w, "Failed to fetch top anime", http.StatusInternalServerError) + return + } + + templates.CatalogItems(res.Animes, page+1, res.HasNextPage).Render(r.Context(), w) +} + +func (h *AnimeHandler) HandleAnimeDetails(w http.ResponseWriter, r *http.Request) { + idStr := r.URL.Path[len("/anime/"):] + id, err := strconv.Atoi(idStr) + if err != nil || id <= 0 { + http.NotFound(w, r) + return + } + + anime, err := h.jikan.GetAnimeByID(id) + if err != nil { + log.Printf("anime fetch error for %d: %v", id, err) + http.Error(w, "Failed to fetch anime details", http.StatusInternalServerError) + return + } + + // Get current watchlist status if user is logged in + currentStatus := "" + if user := middleware.GetUser(r.Context()); user != nil { + entry, err := h.db.GetWatchListEntry(r.Context(), database.GetWatchListEntryParams{ + UserID: user.ID, + AnimeID: int64(id), + }) + if err == nil { + currentStatus = entry.Status + } + } + + templates.AnimeDetails(anime, currentStatus).Render(r.Context(), w) +} + +func (h *AnimeHandler) HandleAPIAnimeRelations(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path[len("/api/anime/"):] + idStr := "" + for i, c := range path { + if c == '/' { + idStr = path[:i] + break + } + } + + id, _ := strconv.Atoi(idStr) + if id <= 0 { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + + relations := h.jikan.GetFullRelations(id) + templates.AnimeRelationsList(relations).Render(r.Context(), w) +} diff --git a/internal/server/routes.go b/internal/server/routes.go new file mode 100644 index 0000000..d5c84db --- /dev/null +++ b/internal/server/routes.go @@ -0,0 +1,58 @@ +package server + +import ( + "net/http" + + "malago/internal/auth" + "malago/internal/database" + "malago/internal/handlers" + "malago/internal/jikan" + "malago/internal/middleware" +) + +type Config struct { + DB *database.Queries + JikanClient *jikan.Client + AuthService *auth.Service +} + +func NewRouter(cfg Config) http.Handler { + mux := http.NewServeMux() + + authHandler := handlers.NewAuthHandler(cfg.AuthService) + watchlistHandler := handlers.NewWatchlistHandler(cfg.DB) + animeHandler := handlers.NewAnimeHandler(cfg.JikanClient, cfg.DB) + + // Serve static files + fs := http.FileServer(http.Dir("./static")) + mux.Handle("/static/", http.StripPrefix("/static/", fs)) + + // Anime / Search / Catalog + mux.HandleFunc("/", animeHandler.HandleCatalog) + mux.HandleFunc("/search", animeHandler.HandleSearch) + mux.HandleFunc("/api/search", animeHandler.HandleAPISearch) + mux.HandleFunc("/api/catalog", animeHandler.HandleAPICatalog) + mux.HandleFunc("/anime/", animeHandler.HandleAnimeDetails) + mux.HandleFunc("/api/anime/", animeHandler.HandleAPIAnimeRelations) + + // Auth Endpoints + mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + authHandler.HandleLoginPage(w, r) + } else { + authHandler.HandleLogin(w, r) + } + }) + mux.HandleFunc("/logout", authHandler.HandleLogout) + + // Watchlist POST endpoint (Protected) + mux.Handle("/api/watchlist/export", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleExportWatchlist))) + mux.Handle("/api/watchlist/import", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleImportWatchlist))) + mux.Handle("/api/watchlist", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleUpdateWatchlist))) + mux.Handle("/api/watchlist/", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleDeleteWatchlist))) + mux.Handle("/watchlist", middleware.RequireAuth(http.HandlerFunc(watchlistHandler.HandleGetWatchlist))) + + // Wrap mux with global auth checking, THEN auth context parsing + protectedHandler := middleware.RequireGlobalAuth(mux) + return middleware.Auth(cfg.AuthService)(protectedHandler) +}