package main import ( "bookmann/internal/config" "bookmann/internal/database" "bookmann/internal/handlers" "embed" "log" "mime" "net/http" "path/filepath" "strings" "github.com/go-playground/validator/v10" jwtgo "github.com/golang-jwt/jwt" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) //go:embed static/* var staticFS embed.FS // 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()} // 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) // Routes handlers.SetupRoutes(protected, queries) // Serve static files with SPA fallback from embedded FS e.GET("/*", func(c echo.Context) error { path := c.Request().URL.Path if strings.HasPrefix(path, "/api") { return c.String(http.StatusNotFound, "Not found") } // Try to serve the file from embedded FS filePath := "static" + path if file, err := staticFS.Open(filePath); err == nil { defer file.Close() contentType := mime.TypeByExtension(filepath.Ext(filePath)) if contentType == "" { contentType = "application/octet-stream" } return c.Stream(http.StatusOK, contentType, file) } // Try .html extension htmlPath := filePath + ".html" if file, err := staticFS.Open(htmlPath); err == nil { defer file.Close() return c.Stream(http.StatusOK, "text/html", file) } // Try /index.html indexPath := filePath + "/index.html" if file, err := staticFS.Open(indexPath); err == nil { defer file.Close() return c.Stream(http.StatusOK, "text/html", file) } // Fallback to index.html file, _ := staticFS.Open("static/index.html") defer file.Close() return c.Stream(http.StatusOK, "text/html", file) }) // Start server log.Printf("Starting server on port %s", cfg.ServerPort) e.Logger.Fatal(e.Start(":" + cfg.ServerPort)) }