package router import ( "database/sql" "net/http" "os" "path/filepath" "strings" "git.godopu.com/lab/landing_page/backend/internal/handlers" "git.godopu.com/lab/landing_page/backend/internal/httpx" "git.godopu.com/lab/landing_page/backend/internal/repository" "github.com/gin-gonic/gin" ) func SetupRouter(db *sql.DB, adminToken string, corsOrigins ...string) *gin.Engine { r := gin.Default() allowOrigin := "*" if len(corsOrigins) > 0 && corsOrigins[0] != "" { allowOrigin = corsOrigins[0] } // CORS Middleware r.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", allowOrigin) c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) return } c.Next() }) // Repositories homeRepo := repository.NewHomeRepository(db) membersRepo := repository.NewMembersRepository(db) pubsRepo := repository.NewPublicationsRepository(db) lecturesRepo := repository.NewLecturesRepository(db) standardsRepo := repository.NewStandardizationRepository(db) // Handlers homeHandler := handlers.NewHomeHandler(homeRepo) membersHandler := handlers.NewMembersHandler(membersRepo) pubsHandler := handlers.NewPublicationsHandler(pubsRepo) lecturesHandler := handlers.NewLecturesHandler(lecturesRepo) standardsHandler := handlers.NewStandardizationHandler(standardsRepo) // API v1 Public Group (Read-only) v1 := r.Group("/api/v1") { v1.GET("/health", func(c *gin.Context) { httpx.OK(c, gin.H{"status": "ok", "service": "anl-backend-api"}) }) // Home v1.GET("/research-projects", homeHandler.GetResearchProjects) v1.GET("/research-areas", homeHandler.GetResearchAreas) v1.GET("/stats/summary", homeHandler.GetStatsSummary) // Members & Alumni v1.GET("/members", membersHandler.GetMembers) v1.GET("/alumni", membersHandler.GetAlumni) // Publications & Patents v1.GET("/publications", pubsHandler.GetPublications) v1.GET("/publications/highlights", pubsHandler.GetHighlights) v1.GET("/patents", pubsHandler.GetPatents) // Lectures v1.GET("/semesters", lecturesHandler.GetSemesters) // Standardization v1.GET("/standards-bodies", standardsHandler.GetStandardsBodies) } // API v1 Admin Group (Mutating operations requiring Bearer token) admin := v1.Group("") admin.Use(httpx.RequireAdminToken(adminToken)) { admin.GET("/admin/verify", func(c *gin.Context) { httpx.OK(c, gin.H{"status": "authenticated"}) }) // Research Projects admin.POST("/research-projects", homeHandler.CreateResearchProject) admin.PUT("/research-projects/:id", homeHandler.UpdateResearchProject) admin.DELETE("/research-projects/:id", homeHandler.DeleteResearchProject) // Research Areas admin.POST("/research-areas", homeHandler.CreateResearchArea) admin.PUT("/research-areas/:id", homeHandler.UpdateResearchArea) admin.DELETE("/research-areas/:id", homeHandler.DeleteResearchArea) // Members & Alumni admin.POST("/members", membersHandler.CreateMember) admin.PUT("/members/:id", membersHandler.UpdateMember) admin.DELETE("/members/:id", membersHandler.DeleteMember) admin.POST("/alumni", membersHandler.CreateAlumnus) admin.PUT("/alumni/:id", membersHandler.UpdateAlumnus) admin.DELETE("/alumni/:id", membersHandler.DeleteAlumnus) // Publications & Patents admin.POST("/publications", pubsHandler.CreatePublication) admin.PUT("/publications/:id", pubsHandler.UpdatePublication) admin.DELETE("/publications/:id", pubsHandler.DeletePublication) admin.POST("/patents", pubsHandler.CreatePatent) admin.PUT("/patents/:id", pubsHandler.UpdatePatent) admin.DELETE("/patents/:id", pubsHandler.DeletePatent) // Lectures (Semesters & Courses) admin.POST("/semesters", lecturesHandler.CreateSemester) admin.PUT("/semesters/:id", lecturesHandler.UpdateSemester) admin.DELETE("/semesters/:id", lecturesHandler.DeleteSemester) admin.POST("/semesters/:semesterId/courses", lecturesHandler.CreateCourse) admin.PUT("/courses/:id", lecturesHandler.UpdateCourse) admin.DELETE("/courses/:id", lecturesHandler.DeleteCourse) // Standardization (Bodies & Documents) admin.POST("/standards-bodies", standardsHandler.CreateStandardsBody) admin.PUT("/standards-bodies/:id", standardsHandler.UpdateStandardsBody) admin.DELETE("/standards-bodies/:id", standardsHandler.DeleteStandardsBody) admin.POST("/standards-bodies/:bodyId/documents", standardsHandler.CreateStandardDocument) admin.PUT("/standard-documents/:id", standardsHandler.UpdateStandardDocument) admin.DELETE("/standard-documents/:id", standardsHandler.DeleteStandardDocument) } // 308 Permanent Redirect for /intro -> / (H-11 / IA-02) r.GET("/intro", func(c *gin.Context) { c.Redirect(http.StatusPermanentRedirect, "/") }) // Static assets serving r.Static("/_next", "./web/_next") r.StaticFile("/favicon.ico", "./web/favicon.ico") r.StaticFile("/robots.txt", "./web/robots.txt") r.StaticFile("/sitemap.xml", "./web/sitemap.xml") r.StaticFile("/icon.svg", "./web/icon.svg") r.Static("/fonts", "./web/fonts") // NoRoute fallback for static exported HTML & 404 r.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path // Never let a mistyped/unknown API path fall through to HTML. if strings.HasPrefix(path, "/api/") { httpx.NotFound(c, "Route not found") return } // Try an exact static file (e.g. an asset with an extension) candidate := filepath.Join("./web", filepath.Clean(path)) if info, err := os.Stat(candidate); err == nil && !info.IsDir() { c.File(candidate) return } // Try /index.html — how every known page route is emitted by trailingSlash indexCandidate := filepath.Join("./web", filepath.Clean(path), "index.html") if _, err := os.Stat(indexCandidate); err == nil { c.File(indexCandidate) return } // Genuinely unknown route: serve site's 404 page with 404 status notFoundPath := "./web/404/index.html" if _, err := os.Stat(notFoundPath); err != nil { notFoundPath = "./web/404.html" } if notFoundBytes, err := os.ReadFile(notFoundPath); err == nil { c.Data(http.StatusNotFound, "text/html; charset=utf-8", notFoundBytes) } else { c.String(http.StatusNotFound, "404 page not found") } }) return r }