package server import ( "net/http" "github.com/go-chi/chi/v5" chiMiddleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/prabhupadasays/spsays-bff/internal/config" "github.com/prabhupadasays/spsays-bff/internal/handler" "github.com/prabhupadasays/spsays-bff/internal/middleware" "github.com/prabhupadasays/spsays-bff/internal/repository" "go.uber.org/zap" ) // Server holds all dependencies for the HTTP server. type Server struct { cfg *config.Config logger *zap.Logger router chi.Router mongo *repository.MongoDB // Handlers bookH *handler.BookHandler letterH *handler.LetterHandler essayH *handler.EssayHandler glossaryH *handler.GlossaryHandler discipleH *handler.DiscipleHandler journalH *handler.JournalHandler magazineH *handler.MagazineHandler otherDocH *handler.OtherDocumentHandler transcriptionH *handler.TranscriptionHandler } // New creates a new Server with all routes registered. func New(cfg *config.Config, logger *zap.Logger) *Server { mongoDB, err := repository.NewMongoDB(cfg.MongoURI, cfg.MongoDB, logger) if err != nil { logger.Fatal("failed to connect to MongoDB", zap.Error(err)) } // Initialize repositories bookRepo := repository.NewBookRepo(mongoDB) letterRepo := repository.NewLetterRepo(mongoDB) essayRepo := repository.NewEssayRepo(mongoDB) glossaryRepo := repository.NewGlossaryRepo(mongoDB) discipleRepo := repository.NewDiscipleRepo(mongoDB) journalRepo := repository.NewJournalRepo(mongoDB) magazineRepo := repository.NewMagazineRepo(mongoDB) otherDocRepo := repository.NewOtherDocumentRepo(mongoDB) transcriptionRepo := repository.NewTranscriptionRepo(mongoDB) s := &Server{ cfg: cfg, logger: logger, mongo: mongoDB, // Initialize handlers bookH: handler.NewBookHandler(bookRepo), letterH: handler.NewLetterHandler(letterRepo), essayH: handler.NewEssayHandler(essayRepo), glossaryH: handler.NewGlossaryHandler(glossaryRepo), discipleH: handler.NewDiscipleHandler(discipleRepo), journalH: handler.NewJournalHandler(journalRepo), magazineH: handler.NewMagazineHandler(magazineRepo), otherDocH: handler.NewOtherDocumentHandler(otherDocRepo), transcriptionH: handler.NewTranscriptionHandler(transcriptionRepo), } s.setupRouter() return s } // Router returns the chi router. func (s *Server) Router() http.Handler { return s.router } // Close shuts down all connections gracefully. func (s *Server) Close() { if err := s.mongo.Close(); err != nil { s.logger.Error("failed to close MongoDB connection", zap.Error(err)) } // TODO: close PostgreSQL, Redis, etc. connections s.logger.Info("all connections closed") } // injectBookCode is a middleware that sets "code" in chi's URL params // for explicit sub-routes like /books/sb and /books/cc where {code} // isn't a URL param but a fixed path prefix. func injectBookCode(code string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rctx := chi.RouteContext(r.Context()) rctx.URLParams.Add("code", code) next.ServeHTTP(w, r) }) } } func (s *Server) setupRouter() { r := chi.NewRouter() // Global middleware r.Use(chiMiddleware.RequestID) r.Use(chiMiddleware.RealIP) r.Use(middleware.Logger(s.logger)) r.Use(chiMiddleware.Recoverer) r.Use(chiMiddleware.Compress(5)) // CORS r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{s.cfg.FrontendURL}, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Request-ID"}, ExposedHeaders: []string{"X-Request-ID"}, AllowCredentials: true, MaxAge: 300, })) // Health check r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) }) // API v1 r.Route("/api/v1", func(r chi.Router) { // Books r.Route("/books", func(r chi.Router) { r.Get("/", s.bookH.ListBooks) // --- Verse books with divisions --- // SB — canto divisions r.Route("/sb", func(r chi.Router) { r.Use(injectBookCode("sb")) r.Get("/", s.bookH.GetBook) r.Get("/{division}", s.bookH.GetDivisionInfo) r.Get("/{division}/{chapter}", s.bookH.GetDivisionChapterVerseIndex) r.Get("/{division}/{chapter}/verses", s.bookH.GetDivisionChapterVerses) r.Get("/{division}/{chapter}/{verse}", s.bookH.GetDivisionVerse) }) // CC — līlā divisions r.Route("/cc", func(r chi.Router) { r.Use(injectBookCode("cc")) r.Get("/", s.bookH.GetBook) r.Get("/{division}", s.bookH.GetDivisionInfo) r.Get("/{division}/{chapter}", s.bookH.GetDivisionChapterVerseIndex) r.Get("/{division}/{chapter}/verses", s.bookH.GetDivisionChapterVerses) r.Get("/{division}/{chapter}/{verse}", s.bookH.GetDivisionVerse) }) // --- Verse books without divisions --- // BG, SB1962: pure verse books for _, code := range []string{"bg", "sb1962"} { code := code // capture for closure r.Route("/"+code, func(r chi.Router) { r.Use(injectBookCode(code)) r.Get("/", s.bookH.GetBook) r.Get("/{chapter}", s.bookH.GetChapterVerseIndex) r.Get("/{chapter}/verses", s.bookH.GetChapterVerses) r.Get("/{chapter}/{verse}", s.bookH.GetVerse) }) } // --- Verse + prose intro books (no division) --- // BS, MM, NBS: have both verses and prose intro segments for _, code := range []string{"bs", "mm", "nbs"} { code := code r.Route("/"+code, func(r chi.Router) { r.Use(injectBookCode(code)) r.Get("/", s.bookH.GetBook) r.Get("/chapters", s.bookH.ListProseChapters) r.Get("/chapters/{slug}", s.bookH.GetProseChapter) r.Get("/{chapter}", s.bookH.GetChapterVerseIndex) r.Get("/{chapter}/verses", s.bookH.GetChapterVerses) r.Get("/{chapter}/{verse}", s.bookH.GetVerse) }) } // --- Verse + prose intro books with special slugs --- // ISO, NOI: mantras/texts accessed by slug, plus prose intros for _, code := range []string{"iso", "noi"} { code := code r.Route("/"+code, func(r chi.Router) { r.Use(injectBookCode(code)) r.Get("/", s.bookH.GetBook) r.Get("/chapters", s.bookH.ListProseChapters) r.Get("/chapters/{slug}", s.bookH.GetProseChapter) r.Get("/{chapter}", s.bookH.GetChapterVerseIndex) r.Get("/{chapter}/verses", s.bookH.GetChapterVerses) r.Get("/{chapter}/{verse}", s.bookH.GetVerse) }) } // --- Volume-based prose book --- // KB: 2 volumes, chapters within each volume r.Route("/kb", func(r chi.Router) { r.Use(injectBookCode("kb")) r.Get("/", s.bookH.GetBook) r.Get("/{volume}", s.bookH.ListVolumeChapters) r.Get("/{volume}/{slug}", s.bookH.GetVolumeChapter) }) // --- Simple prose books --- // BBD, BK, CBHC, DS, EJ, EK, GG, HKH, LCL, LOB, MG, MOG, NOD, // OCHKM, OWK, PK, POY, PQPA, RV, SSR, SVA, TLC, TLK, TQK, TTP, VA for _, code := range []string{ "bbd", "bk", "cbhc", "ds", "ej", "ek", "gg", "hkh", "lcl", "lob", "mg", "mog", "nod", "ochkm", "owk", "pk", "poy", "pqpa", "rv", "ssr", "sva", "tlc", "tlk", "tqk", "ttp", "va", } { code := code r.Route("/"+code, func(r chi.Router) { r.Use(injectBookCode(code)) r.Get("/", s.bookH.GetBook) r.Get("/{slug}", s.bookH.GetProseChapter) r.Get("/chapters", s.bookH.ListProseChapters) r.Get("/chapters/{slug}", s.bookH.GetProseChapter) }) } }) // Letters r.Route("/letters", func(r chi.Router) { r.Get("/", s.letterH.Count) r.Get("/years", s.letterH.GetYears) r.Get("/recipients", s.letterH.GetRecipients) r.Get("/locations", s.letterH.GetLocations) r.Get("/year/{year}", s.letterH.GetByYear) r.Get("/recipient/{name}", s.letterH.GetByRecipient) r.Get("/location/{name}", s.letterH.GetByLocation) r.Get("/{slug}", s.letterH.GetBySlug) }) // Essays r.Route("/essays", func(r chi.Router) { r.Get("/", s.essayH.List) r.Get("/{slug}", s.essayH.GetBySlug) }) // Glossary r.Route("/glossary", func(r chi.Router) { r.Get("/", s.glossaryH.List) r.Get("/letters", s.glossaryH.GetLetterCounts) r.Get("/letter/{letter}", s.glossaryH.GetByLetter) r.Get("/letter/{letter}/terms", s.glossaryH.GetTermsByLetter) r.Get("/term/{term}", s.glossaryH.GetByTerm) }) // Disciples r.Route("/disciples", func(r chi.Router) { r.Get("/", s.discipleH.List) r.Get("/count", s.discipleH.Count) r.Get("/letters", s.discipleH.LetterCounts) r.Get("/suffixes", s.discipleH.SuffixCounts) r.Get("/search", s.discipleH.Search) }) // Journals r.Route("/journals", func(r chi.Router) { r.Get("/", s.journalH.ListJournals) r.Get("/{book}", s.journalH.GetJournal) r.Get("/{book}/chapters", s.journalH.ListChapters) r.Get("/{book}/chapters/{slug}", s.journalH.GetChapter) }) // Magazines r.Route("/magazines", func(r chi.Router) { r.Get("/", s.magazineH.ListMagazines) r.Get("/{book}", s.magazineH.GetMagazine) r.Get("/{book}/issues", s.magazineH.ListIssues) r.Get("/{book}/issues/{slug}", s.magazineH.GetIssue) }) // Other Documents r.Route("/other-documents", func(r chi.Router) { r.Get("/", s.otherDocH.ListDocuments) r.Get("/{book}", s.otherDocH.GetDocument) r.Get("/{book}/chapters", s.otherDocH.ListChapters) r.Get("/{book}/chapters/{slug}", s.otherDocH.GetChapter) }) // Transcriptions r.Route("/transcriptions", func(r chi.Router) { r.Get("/", s.transcriptionH.Count) r.Get("/years", s.transcriptionH.GetYears) r.Get("/places", s.transcriptionH.GetPlaces) r.Get("/types", s.transcriptionH.GetTypeDistribution) r.Get("/year/{year}", s.transcriptionH.GetByYear) r.Get("/place/{place}", s.transcriptionH.GetByPlace) r.Get("/type/{type}", s.transcriptionH.GetByType) r.Get("/adjacent/{slug}", s.transcriptionH.GetAdjacent) r.Get("/{slug}", s.transcriptionH.GetBySlug) }) // Search (Meilisearch) r.Route("/search", func(r chi.Router) { // TODO: register search handlers }) // User routes (PostgreSQL) r.Route("/auth", func(r chi.Router) { // TODO: register auth handlers (login, register, refresh) }) r.Route("/users", func(r chi.Router) { // TODO: register user handlers }) r.Route("/collections", func(r chi.Router) { // TODO: register collection/bookmark handlers }) r.Route("/mentions", func(r chi.Router) { // TODO: register mention handlers }) // URL Shortener r.Route("/s", func(r chi.Router) { // TODO: register URL shortener handlers }) // Push Notifications r.Route("/notifications", func(r chi.Router) { // TODO: register push notification handlers }) }) s.router = r }