package server import ( "errors" "io" "io/fs" "net/http" "path" "strings" "git.javil.eu/jacob1123/budgeteer" "git.javil.eu/jacob1123/budgeteer/bcrypt" "git.javil.eu/jacob1123/budgeteer/postgres" "github.com/gin-gonic/gin" ) // Handler handles incoming requests. type Handler struct { Service *postgres.Database TokenVerifier budgeteer.TokenVerifier CredentialsVerifier *bcrypt.Verifier StaticFS http.FileSystem } // Serve starts the http server. func (h *Handler) Serve() { router := gin.Default() h.LoadRoutes(router) if err := router.Run(":1323"); err != nil { panic(err) } } type ErrorResponse struct { Message string } // LoadRoutes initializes all the routes. func (h *Handler) LoadRoutes(router *gin.Engine) { router.Use(enableCachingForStaticFiles()) router.NoRoute(h.ServeStatic) withLogin := router.Group("") withLogin.Use(h.verifyLoginWithRedirect) withBudget := router.Group("") withBudget.Use(h.verifyLoginWithForbidden) withBudget.GET("/budget/:budgetid/:year/:month", h.budgeting) withBudget.GET("/budget/:budgetid/settings/clean-negative", h.cleanNegativeBudget) api := router.Group("/api/v1") unauthenticated := api.Group("/user") unauthenticated.GET("/login", func(c *gin.Context) { c.Redirect(http.StatusPermanentRedirect, "/login") }) unauthenticated.POST("/login", h.loginPost) unauthenticated.POST("/register", h.registerPost) authenticated := api.Group("") authenticated.Use(h.verifyLoginWithForbidden) authenticated.GET("/dashboard", h.dashboard) authenticated.GET("/account/:accountid/transactions", h.transactionsForAccount) authenticated.GET("/admin/clear-database", h.clearDatabase) authenticated.GET("/budget/:budgetid", h.budgeting) authenticated.GET("/budget/:budgetid/:year/:month", h.budgetingForMonth) authenticated.GET("/budget/:budgetid/autocomplete/payees", h.autocompletePayee) authenticated.GET("/budget/:budgetid/autocomplete/categories", h.autocompleteCategories) authenticated.DELETE("/budget/:budgetid", h.deleteBudget) authenticated.POST("/budget/:budgetid/import/ynab", h.importYNAB) authenticated.POST("/budget/:budgetid/export/ynab/transactions", h.exportYNABTransactions) authenticated.POST("/budget/:budgetid/export/ynab/assignments", h.exportYNABAssignments) authenticated.POST("/budget/:budgetid/settings/clear", h.clearBudget) budget := authenticated.Group("/budget") budget.POST("/new", h.newBudget) transaction := authenticated.Group("/transaction") transaction.POST("/new", h.newTransaction) transaction.POST("/:transactionid", h.newTransaction) } func (h *Handler) ServeStatic(c *gin.Context) { h.ServeStaticFile(c, c.Request.URL.Path) } func (h *Handler) ServeStaticFile(c *gin.Context, fullPath string) { file, err := h.StaticFS.Open(fullPath) if errors.Is(err, fs.ErrNotExist) { h.ServeStaticFile(c, path.Join("/", "/index.html")) return } if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } stat, err := file.Stat() if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } if stat.IsDir() { h.ServeStaticFile(c, path.Join(fullPath, "index.html")) return } if file, ok := file.(io.ReadSeeker); ok { http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), file) } else { panic("File does not implement ReadSeeker") } } func enableCachingForStaticFiles() gin.HandlerFunc { return func(c *gin.Context) { if strings.HasPrefix(c.Request.RequestURI, "/static/") { c.Header("Cache-Control", "max-age=86400") } } }