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 } type SuccessResponse struct { Message string } // LoadRoutes initializes all the routes. func (h *Handler) LoadRoutes(router *gin.Engine) { router.Use(enableCachingForStaticFiles()) router.NoRoute(h.ServeStatic) api := router.Group("/api/v1") anonymous := api.Group("/user") anonymous.POST("/login", h.loginPost) anonymous.POST("/register", h.registerPost) authenticated := api.Group("") { authenticated.Use(h.verifyLoginWithForbidden) authenticated.GET("/account/:accountid/transactions", h.transactionsForAccount) authenticated.POST("/account/:accountid/reconcile", h.reconcileTransactions) authenticated.POST("/account/:accountid", h.editAccount) authenticated.GET("/admin/clear-database", h.clearDatabase) budget := authenticated.Group("/budget") budget.POST("/new", h.newBudget) budget.GET("/:budgetid", h.budget) budget.GET("/:budgetid/:year/:month", h.budgetingForMonth) budget.POST("/:budgetid/category/:categoryid/:year/:month", h.setCategoryAssignment) budget.GET("/:budgetid/autocomplete/payees", h.autocompletePayee) budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories) budget.GET("/:budgetid/problematic-transactions", h.problematicTransactions) budget.DELETE("/:budgetid", h.deleteBudget) budget.POST("/:budgetid/import/ynab", h.importYNAB) budget.POST("/:budgetid/export/ynab/transactions", h.exportYNABTransactions) budget.POST("/:budgetid/export/ynab/assignments", h.exportYNABAssignments) budget.POST("/:budgetid/settings/clear", h.clearBudget) transaction := authenticated.Group("/transaction") transaction.POST("/new", h.newTransaction) transaction.POST("/:transactionid", h.updateTransaction) } } 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") } } }