package main import ( "context" "fmt" "html/template" "log" "net/http" "os" "os/signal" "path/filepath" "strings" "syscall" "time" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/HoHD/PR-Helper/config" "github.com/HoHD/PR-Helper/database" "github.com/HoHD/PR-Helper/handlers" ) func main() { cfg := config.Load() db, err := database.New(cfg.MySQLDSN()) if err != nil { log.Fatalf("failed to initialize database: %v", err) } defer db.Close() gin.SetMode(cfg.GinMode) r := gin.Default() // Session middleware store := cookie.NewStore([]byte(cfg.SessionSecret)) store.Options(sessions.Options{ MaxAge: 86400 * 7, // 7 days Path: "/", HttpOnly: true, }) r.Use(sessions.Sessions("pr_session", store)) // Custom template functions funcMap := template.FuncMap{ "add": func(a, b int) int { return a + b }, "formatSize": func(bytes int64) string { if bytes < 1024 { return fmt.Sprintf("%d B", bytes) } if bytes < 1024*1024 { return fmt.Sprintf("%.1f KB", float64(bytes)/1024) } return fmt.Sprintf("%.1f MB", float64(bytes)/1024/1024) }, } // Load templates: base layout first, then pages and partials tmpl := template.New("").Funcs(funcMap) // Walk all template files and parse them together filepath.Walk("templates", func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || !strings.HasSuffix(path, ".html") { return nil } data, readErr := os.ReadFile(path) if readErr != nil { return readErr } // Use the relative path as template name (e.g. "layouts/base.html") name := strings.TrimPrefix(path, "templates/") t, parseErr := tmpl.New(name).Parse(string(data)) if parseErr != nil { log.Fatalf("failed to parse template %s: %v", name, parseErr) } tmpl = t return nil }) r.SetHTMLTemplate(tmpl) // Static files r.Static("/static", "./static") // Handlers auth := handlers.NewAuthHandler(db.Conn()) pages := handlers.NewPageHandler(db.Conn()) settings := handlers.NewSettingsHandler(db.Conn()) repos := handlers.NewReposHandler(db.Conn(), cfg.ReposDir()) generate := handlers.NewGenerateHandler(db.Conn()) review := handlers.NewReviewHandler(db.Conn()) // Public routes (no auth required) r.GET("/login", auth.Login) r.GET("/register", auth.Register) r.POST("/api/auth/login", auth.HandleLogin) r.POST("/api/auth/register", auth.HandleRegister) r.POST("/api/auth/logout", auth.HandleLogout) // Protected routes (auth required) authMw := handlers.AuthRequired(db.Conn()) // Page routes r.GET("/", authMw, pages.Index) r.GET("/repo/:id", authMw, pages.Repo) r.GET("/repo/:id/generate", authMw, pages.Generate) r.GET("/repo/:id/review", authMw, pages.Review) r.GET("/settings", authMw, pages.Settings) // API routes r.GET("/api/settings", authMw, settings.GetSettings) r.PUT("/api/settings", authMw, settings.UpdateSettings) r.GET("/api/repos", authMw, repos.ListRepos) r.POST("/api/repos", authMw, repos.CloneRepo) r.DELETE("/api/repos/:id", authMw, repos.DeleteRepo) r.POST("/api/repos/:id/cleanup", authMw, repos.CleanupRepos) r.POST("/api/repos/:id/pull", authMw, repos.PullRepo) r.GET("/api/repos/:id/graph", authMw, repos.GetGraph) r.GET("/api/repos/:id/refs", authMw, repos.GetRefs) r.GET("/api/repos/:id/commits", authMw, repos.GetCommits) r.GET("/api/repos/:id/diff", authMw, repos.GetDiff) r.POST("/api/repos/:id/generate", authMw, generate.Generate) r.POST("/api/repos/:id/review", authMw, review.Review) r.GET("/api/repos/:id/review/analyses", authMw, review.ListReviews) r.GET("/api/repos/:id/review/analyses/:aid", authMw, review.GetReview) r.POST("/api/repos/:id/review/notes", authMw, review.SaveNotes) r.GET("/api/repos/:id/review/notes", authMw, review.GetNotes) // Graceful shutdown with signal handling srv := &http.Server{ Addr: ":" + cfg.Port, Handler: r, } go func() { log.Printf("PR-Helper starting on :%s", cfg.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("failed to start server: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatalf("server forced to shutdown: %v", err) } log.Println("server exited") }