package http import ( "context" "io/fs" "net/http" "time" "git.javil.eu/jacob1123/budgeteer" "git.javil.eu/jacob1123/budgeteer/bcrypt" "git.javil.eu/jacob1123/budgeteer/postgres" "git.javil.eu/jacob1123/budgeteer/web" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/jackc/pgtype" ) // Handler handles incoming requests type Handler struct { Service *postgres.Repository TokenVerifier budgeteer.TokenVerifier CredentialsVerifier *bcrypt.Verifier } const ( expiration = 72 authCookie = "authentication" ) // Serve starts the HTTP Server func (h *Handler) Serve() { router := gin.Default() templates, err := NewTemplates(router.FuncMap) if err != nil { panic(err) } router.HTMLRender = templates static, err := fs.Sub(web.Static, "static") if err != nil { panic("couldn't open static files") } router.StaticFS("/static", http.FS(static)) router.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) }) router.GET("/login", h.login) router.GET("/register", h.register) authenticatedFrontend := router.Group("") { authenticatedFrontend.Use(h.verifyLoginWithRedirect) authenticatedFrontend.GET("/dashboard", h.dashboard) authenticatedFrontend.GET("/budget/:budgetid", h.budget) authenticatedFrontend.GET("/budget/:budgetid/accounts", h.accounts) } api := router.Group("/api/v1") { user := api.Group("/user") { unauthenticated := user.Group("") { unauthenticated.GET("/login", func(c *gin.Context) { c.Redirect(http.StatusPermanentRedirect, "/login") }) unauthenticated.POST("/login", h.loginPost) unauthenticated.POST("/register", h.registerPost) } authenticated := user.Group("") { authenticated.Use(h.verifyLoginWithRedirect) authenticated.GET("/logout", logout) } } budget := api.Group("/budget") { budget.Use(h.verifyLoginWithRedirect) budget.POST("/new", h.newBudget) } transaction := api.Group("/transaction") { transaction.Use(h.verifyLoginWithRedirect) transaction.POST("/new", h.newTransaction) transaction.POST("/import/ynab", h.importYNAB) } } router.Run(":1323") } func (h *Handler) importYNAB(c *gin.Context) { transactionsFile, err := c.FormFile("transactions") if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } transactions, err := transactionsFile.Open() if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } budgetID, succ := c.GetPostForm("budget_id") if !succ { c.AbortWithError(http.StatusBadRequest, err) return } budgetUUID, err := uuid.Parse(budgetID) if !succ { c.AbortWithError(http.StatusBadRequest, err) return } ynab, err := NewYNABImport(h.Service.DB, budgetUUID) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } err = ynab.Import(transactions) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } } func (h *Handler) verifyLoginWithRedirect(c *gin.Context) { _, err := h.verifyLogin(c) if err != nil { c.Redirect(http.StatusTemporaryRedirect, "/login") return } c.Next() } func (h *Handler) newTransaction(c *gin.Context) { transactionMemo, succ := c.GetPostForm("memo") if !succ { c.AbortWithStatus(http.StatusNotAcceptable) return } transactionAccount, succ := c.GetPostForm("account_id") if !succ { c.AbortWithStatus(http.StatusNotAcceptable) return } transactionAccountID, err := uuid.Parse(transactionAccount) if !succ { c.AbortWithStatus(http.StatusNotAcceptable) return } transactionDate, succ := c.GetPostForm("date") if !succ { c.AbortWithStatus(http.StatusNotAcceptable) return } transactionDateValue, err := time.Parse("2006-01-02", transactionDate) if err != nil { c.AbortWithStatus(http.StatusNotAcceptable) return } new := postgres.CreateTransactionParams{ Memo: transactionMemo, Date: transactionDateValue, Amount: pgtype.Numeric{}, AccountID: transactionAccountID, } _, err = h.Service.DB.CreateTransaction(c.Request.Context(), new) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } } func (h *Handler) newBudget(c *gin.Context) { token, err := h.verifyLogin(c) if err != nil { c.Redirect(http.StatusTemporaryRedirect, "/login") return } budgetName, succ := c.GetPostForm("name") if !succ { c.AbortWithStatus(http.StatusNotAcceptable) return } _, err = h.Service.NewBudget(budgetName, token.GetID()) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } } func (h *Handler) verifyLogin(c *gin.Context) (budgeteer.Token, error) { tokenString, err := c.Cookie(authCookie) if err != nil { return nil, err } token, err := h.TokenVerifier.VerifyToken(tokenString) if err != nil { c.SetCookie(authCookie, "", -1, "", "", false, false) return nil, err } return token, nil } func (h *Handler) login(c *gin.Context) { if _, err := h.verifyLogin(c); err == nil { c.Redirect(http.StatusTemporaryRedirect, "/dashboard") return } c.HTML(http.StatusOK, "login.html", nil) } func (h *Handler) register(c *gin.Context) { if _, err := h.verifyLogin(c); err == nil { c.Redirect(http.StatusTemporaryRedirect, "/dashboard") return } c.HTML(http.StatusOK, "register.html", nil) } func logout(c *gin.Context) { clearLogin(c) } func clearLogin(c *gin.Context) { c.SetCookie(authCookie, "", -1, "", "", false, true) } func (h *Handler) loginPost(c *gin.Context) { username, _ := c.GetPostForm("username") password, _ := c.GetPostForm("password") user, err := h.Service.DB.GetUserByUsername(context.Background(), username) if err != nil { c.AbortWithError(http.StatusUnauthorized, err) return } if err = h.CredentialsVerifier.Verify(password, user.Password); err != nil { c.AbortWithError(http.StatusUnauthorized, err) return } t, err := h.TokenVerifier.CreateToken(&user) if err != nil { c.AbortWithError(http.StatusUnauthorized, err) } maxAge := (int)((expiration * time.Hour).Seconds()) c.SetCookie(authCookie, t, maxAge, "", "", false, true) c.JSON(http.StatusOK, map[string]string{ "token": t, }) } func (h *Handler) registerPost(c *gin.Context) { email, _ := c.GetPostForm("email") password, _ := c.GetPostForm("password") name, _ := c.GetPostForm("name") _, err := h.Service.DB.GetUserByUsername(context.Background(), email) if err == nil { c.AbortWithStatus(http.StatusUnauthorized) return } hash, err := h.CredentialsVerifier.Hash(password) if err != nil { c.AbortWithError(http.StatusUnauthorized, err) return } createUser := postgres.CreateUserParams{ Name: name, Password: hash, Email: email, } _, err = h.Service.DB.CreateUser(context.Background(), createUser) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) } }