package main import ( "bookmann/internal/config" "bookmann/internal/database" "bookmann/internal/handlers" "html/template" "io" "log" "net/http" "strings" "github.com/go-playground/validator/v10" jwtgo "github.com/golang-jwt/jwt" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) type TemplateRenderer struct { templates *template.Template } func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) } // CustomValidator wraps the go-playground validator type CustomValidator struct { validator *validator.Validate } func (cv *CustomValidator) Validate(i interface{}) error { return cv.validator.Struct(i) } func main() { cfg := config.LoadConfig() pool, err := database.NewConnection(cfg.DatabaseURL()) if err != nil { log.Fatal("Failed to connect to database:", err) } defer pool.Close() queries := database.New(pool) e := echo.New() // Set up validator e.Validator = &CustomValidator{validator: validator.New()} // Set up templates renderer := &TemplateRenderer{ templates: template.Must(template.ParseGlob("templates/*.html")), } e.Renderer = renderer // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORS()) // Auth routes (no auth required) auth := handlers.NewAuthHandler(queries, cfg.JWTSecret) e.POST("/api/auth/register", auth.Register) e.POST("/api/auth/login", auth.Login) // JWT middleware for protected routes jwtMiddleware := middleware.JWTWithConfig(middleware.JWTConfig{ SigningKey: []byte(cfg.JWTSecret), ContextKey: "user", SuccessHandler: func(c echo.Context) { token := c.Get("user").(*jwtgo.Token) claims := token.Claims.(jwtgo.MapClaims) c.Set("user_id", claims["user_id"]) }, }) // Protected routes protected := e.Group("/api", jwtMiddleware) protected.GET("/auth/profile", auth.GetProfile) protected.POST("/auth/ebook-folders", auth.AddEbookFolder) protected.GET("/auth/ebook-folders", auth.GetEbookFolders) protected.DELETE("/auth/ebook-folders/:folderPath", auth.DeleteEbookFolder) protected.GET("/users", auth.ListUsers) // Routes handlers.SetupRoutes(protected, queries) // Static files e.Static("/static", "static") // Page routes e.GET("/", func(c echo.Context) error { // Check if user has valid JWT token tokenString := c.Request().Header.Get("Authorization") if tokenString != "" && strings.HasPrefix(tokenString, "Bearer ") { tokenString = strings.TrimPrefix(tokenString, "Bearer ") token, err := jwtgo.Parse(tokenString, func(token *jwtgo.Token) (interface{}, error) { return []byte(cfg.JWTSecret), nil }) if err == nil && token.Valid { // User is authenticated, get user info and serve dashboard claims := token.Claims.(jwtgo.MapClaims) userID := claims["user_id"].(string) user, err := queries.GetUser(c.Request().Context(), pgtype.UUID{Bytes: uuid.MustParse(userID), Valid: true}) if err == nil { return c.Render(http.StatusOK, "dashboard.html", map[string]interface{}{ "User": map[string]string{ "ID": uuid.UUID(user.ID.Bytes).String(), "Username": user.Username, "Email": user.Email, }, }) } } } // User not authenticated or token invalid, serve landing page return c.Render(http.StatusOK, "index.html", nil) }) e.GET("/login", func(c echo.Context) error { return c.Render(http.StatusOK, "login.html", nil) }) e.GET("/register", func(c echo.Context) error { return c.Render(http.StatusOK, "register.html", nil) }) // Start server log.Printf("Starting server on port %s", cfg.ServerPort) e.Logger.Fatal(e.Start(":" + cfg.ServerPort)) }