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/labstack/echo/v4" ) // 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 := echo.New() h.LoadRoutes(router) if err := router.Start(":1323"); err != nil { panic(err) } } // LoadRoutes initializes all the routes. func (h *Handler) LoadRoutes(router *echo.Echo) { router.Use(enableCachingForStaticFiles()) router.Use(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/accounts", h.autocompleteAccounts) budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories) budget.GET("/:budgetid/problematic-transactions", h.problematicTransactions) budget.POST("/:budgetid/filtered-transactions", h.filteredTransactions) 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(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { h.ServeStaticFile(c, c.Path()) return nil } } func (h *Handler) ServeStaticFile(c echo.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.Error(err) return } stat, err := file.Stat() if err != nil { c.Error(err) return } if stat.IsDir() { h.ServeStaticFile(c, path.Join(fullPath, "index.html")) return } if file, ok := file.(io.ReadSeeker); ok { http.ServeContent(c.Response().Writer, c.Request(), stat.Name(), stat.ModTime(), file) } else { panic("File does not implement ReadSeeker") } } func enableCachingForStaticFiles() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if strings.HasPrefix(c.Path(), "/static/") { c.Response().Header().Set("Cache-Control", "max-age=86400") } return next(c) } } }