Rename http package to server
This commit is contained in:
37
server/account.go
Normal file
37
server/account.go
Normal file
@ -0,0 +1,37 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (h *Handler) transactionsForAccount(c *gin.Context) {
|
||||
accountID := c.Param("accountid")
|
||||
accountUUID, err := uuid.Parse(accountID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
account, err := h.Service.GetAccount(c.Request.Context(), accountUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
transactions, err := h.Service.GetTransactionsForAccount(c.Request.Context(), accountUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, TransactionsResponse{account, transactions})
|
||||
}
|
||||
|
||||
type TransactionsResponse struct {
|
||||
Account postgres.Account
|
||||
Transactions []postgres.GetTransactionsForAccountRow
|
||||
}
|
80
server/account_test.go
Normal file
80
server/account_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/bcrypt"
|
||||
"git.javil.eu/jacob1123/budgeteer/jwt"
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"git.javil.eu/jacob1123/budgeteer/server"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
txdb "github.com/DATA-DOG/go-txdb"
|
||||
)
|
||||
|
||||
func init() {
|
||||
txdb.Register("pgtx", "pgx", "postgres://budgeteer_test:budgeteer_test@localhost:5432/budgeteer_test")
|
||||
}
|
||||
|
||||
func TestListTimezonesHandler(t *testing.T) {
|
||||
database, err := postgres.Connect("pgtx", "example")
|
||||
if err != nil {
|
||||
t.Errorf("could not connect to db: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
h := server.Handler{
|
||||
Service: database,
|
||||
TokenVerifier: &jwt.TokenVerifier{},
|
||||
CredentialsVerifier: &bcrypt.Verifier{},
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
context, engine := gin.CreateTestContext(recorder)
|
||||
h.LoadRoutes(engine)
|
||||
|
||||
t.Run("RegisterUser", func(t *testing.T) {
|
||||
context.Request, err = http.NewRequest(http.MethodPost, "/api/v1/user/register", strings.NewReader(`{"password":"pass","email":"info@example.com","name":"Test"}`))
|
||||
if err != nil {
|
||||
t.Errorf("error creating request: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.registerPost(context)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", recorder.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var response server.LoginResponse
|
||||
err = json.NewDecoder(recorder.Body).Decode(&response)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
t.Error("Error registering")
|
||||
}
|
||||
if len(response.Token) == 0 {
|
||||
t.Error("Did not get a token")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetTransactions", func(t *testing.T) {
|
||||
context.Request, err = http.NewRequest(http.MethodGet, "/account/accountid/transactions", nil)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", recorder.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var response server.TransactionsResponse
|
||||
err = json.NewDecoder(recorder.Body).Decode(&response)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
t.Error("Error retreiving list of transactions.")
|
||||
}
|
||||
if len(response.Transactions) == 0 {
|
||||
t.Error("Did not get any transactions.")
|
||||
}
|
||||
})
|
||||
}
|
112
server/admin.go
Normal file
112
server/admin.go
Normal file
@ -0,0 +1,112 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func (h *Handler) clearDatabase(c *gin.Context) {
|
||||
if err := goose.Reset(h.Service.DB, "schema"); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
if err := goose.Up(h.Service.DB, "schema"); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) deleteBudget(c *gin.Context) {
|
||||
budgetID := c.Param("budgetid")
|
||||
budgetUUID, err := uuid.Parse(budgetID)
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.clearBudgetData(c, budgetUUID)
|
||||
|
||||
err = h.Service.DeleteBudget(c.Request.Context(), budgetUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) clearBudgetData(c *gin.Context, budgetUUID uuid.UUID) {
|
||||
rows, err := h.Service.DeleteAllAssignments(c.Request.Context(), budgetUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Deleted %d assignments\n", rows)
|
||||
|
||||
rows, err = h.Service.DeleteAllTransactions(c.Request.Context(), budgetUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Deleted %d transactions\n", rows)
|
||||
}
|
||||
|
||||
func (h *Handler) clearBudget(c *gin.Context) {
|
||||
budgetID := c.Param("budgetid")
|
||||
budgetUUID, err := uuid.Parse(budgetID)
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.clearBudgetData(c, budgetUUID)
|
||||
}
|
||||
|
||||
func (h *Handler) cleanNegativeBudget(c *gin.Context) {
|
||||
/*budgetID := c.Param("budgetid")
|
||||
budgetUUID, err := uuid.Parse(budgetID)
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||
return
|
||||
}*/
|
||||
|
||||
/*min_date, err := h.Service.GetFirstActivity(c.Request.Context(), budgetUUID)
|
||||
date := getFirstOfMonthTime(min_date)
|
||||
for {
|
||||
nextDate := date.AddDate(0, 1, 0)
|
||||
params := postgres.GetCategoriesWithBalanceParams{
|
||||
BudgetID: budgetUUID,
|
||||
ToDate: nextDate,
|
||||
FromDate: date,
|
||||
}
|
||||
categories, err := h.Service.GetCategoriesWithBalance(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, category := range categories {
|
||||
available := category.Available.GetFloat64()
|
||||
if available >= 0 {
|
||||
continue
|
||||
}
|
||||
var negativeAvailable postgres.Numeric
|
||||
negativeAvailable.Set(-available)
|
||||
createAssignment := postgres.CreateAssignmentParams{
|
||||
Date: nextDate.AddDate(0, 0, -1),
|
||||
Amount: negativeAvailable,
|
||||
CategoryID: category.ID,
|
||||
}
|
||||
h.Service.CreateAssignment(c.Request.Context(), createAssignment)
|
||||
}
|
||||
|
||||
if nextDate.Before(time.Now()) {
|
||||
date = nextDate
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}*/
|
||||
}
|
54
server/autocomplete.go
Normal file
54
server/autocomplete.go
Normal file
@ -0,0 +1,54 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (h *Handler) autocompleteCategories(c *gin.Context) {
|
||||
budgetID := c.Param("budgetid")
|
||||
budgetUUID, err := uuid.Parse(budgetID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("budgetid missing from URL"))
|
||||
return
|
||||
}
|
||||
|
||||
query := c.Request.URL.Query().Get("s")
|
||||
searchParams := postgres.SearchCategoriesParams{
|
||||
BudgetID: budgetUUID,
|
||||
Search: "%" + query + "%",
|
||||
}
|
||||
categories, err := h.Service.SearchCategories(c.Request.Context(), searchParams)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, categories)
|
||||
}
|
||||
|
||||
func (h *Handler) autocompletePayee(c *gin.Context) {
|
||||
budgetID := c.Param("budgetid")
|
||||
budgetUUID, err := uuid.Parse(budgetID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("budgetid missing from URL"))
|
||||
return
|
||||
}
|
||||
|
||||
query := c.Request.URL.Query().Get("s")
|
||||
searchParams := postgres.SearchPayeesParams{
|
||||
BudgetID: budgetUUID,
|
||||
Search: query + "%",
|
||||
}
|
||||
payees, err := h.Service.SearchPayees(c.Request.Context(), searchParams)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, payees)
|
||||
}
|
36
server/budget.go
Normal file
36
server/budget.go
Normal file
@ -0,0 +1,36 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type newBudgetInformation struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (h *Handler) newBudget(c *gin.Context) {
|
||||
var newBudget newBudgetInformation
|
||||
err := c.BindJSON(&newBudget)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusNotAcceptable, err)
|
||||
return
|
||||
}
|
||||
|
||||
if newBudget.Name == "" {
|
||||
c.AbortWithError(http.StatusNotAcceptable, fmt.Errorf("Budget name is needed"))
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.MustGet("token").(budgeteer.Token).GetID()
|
||||
budget, err := h.Service.NewBudget(c.Request.Context(), newBudget.Name, userID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, budget)
|
||||
}
|
216
server/budgeting.go
Normal file
216
server/budgeting.go
Normal file
@ -0,0 +1,216 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func getFirstOfMonth(year, month int, location *time.Location) time.Time {
|
||||
return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, location)
|
||||
}
|
||||
|
||||
func getFirstOfMonthTime(date time.Time) time.Time {
|
||||
var monthM time.Month
|
||||
year, monthM, _ := date.Date()
|
||||
month := int(monthM)
|
||||
return getFirstOfMonth(year, month, date.Location())
|
||||
}
|
||||
|
||||
type CategoryWithBalance struct {
|
||||
*postgres.GetCategoriesRow
|
||||
Available postgres.Numeric
|
||||
AvailableLastMonth postgres.Numeric
|
||||
Activity postgres.Numeric
|
||||
Assigned postgres.Numeric
|
||||
}
|
||||
|
||||
func getDate(c *gin.Context) (time.Time, error) {
|
||||
var year, month int
|
||||
yearString := c.Param("year")
|
||||
monthString := c.Param("month")
|
||||
if yearString == "" && monthString == "" {
|
||||
return getFirstOfMonthTime(time.Now()), nil
|
||||
}
|
||||
|
||||
year, err := strconv.Atoi(yearString)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("parse year: %w", err)
|
||||
}
|
||||
|
||||
month, err = strconv.Atoi(monthString)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("parse month: %w", err)
|
||||
}
|
||||
|
||||
return getFirstOfMonth(year, month, time.Now().Location()), nil
|
||||
}
|
||||
|
||||
func (h *Handler) budgetingForMonth(c *gin.Context) {
|
||||
budgetID := c.Param("budgetid")
|
||||
budgetUUID, err := uuid.Parse(budgetID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("budgetid missing from URL"))
|
||||
return
|
||||
}
|
||||
|
||||
budget, err := h.Service.GetBudget(c.Request.Context(), budgetUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
firstOfMonth, err := getDate(c)
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budgetUUID.String())
|
||||
return
|
||||
}
|
||||
|
||||
categories, err := h.Service.GetCategories(c.Request.Context(), budgetUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
firstOfNextMonth := firstOfMonth.AddDate(0, 1, 0)
|
||||
cumultativeBalances, err := h.Service.GetCumultativeBalances(c.Request.Context(), budgetUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("load balances: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// skip everything in the future
|
||||
categoriesWithBalance, moneyUsed, err := h.calculateBalances(c, budget, firstOfNextMonth, firstOfMonth, categories, cumultativeBalances)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
availableBalance := postgres.NewZeroNumeric()
|
||||
for _, cat := range categories {
|
||||
if cat.ID != budget.IncomeCategoryID {
|
||||
continue
|
||||
}
|
||||
availableBalance = moneyUsed
|
||||
|
||||
for _, bal := range cumultativeBalances {
|
||||
if bal.CategoryID != cat.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
if !bal.Date.Before(firstOfNextMonth) {
|
||||
continue
|
||||
}
|
||||
|
||||
availableBalance = availableBalance.Add(bal.Transactions)
|
||||
}
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Categories []CategoryWithBalance
|
||||
AvailableBalance postgres.Numeric
|
||||
}{categoriesWithBalance, availableBalance}
|
||||
c.JSON(http.StatusOK, data)
|
||||
|
||||
}
|
||||
|
||||
func (h *Handler) budgeting(c *gin.Context) {
|
||||
budgetID := c.Param("budgetid")
|
||||
budgetUUID, err := uuid.Parse(budgetID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("budgetid missing from URL"))
|
||||
return
|
||||
}
|
||||
|
||||
budget, err := h.Service.GetBudget(c.Request.Context(), budgetUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
accounts, err := h.Service.GetAccountsWithBalance(c.Request.Context(), budgetUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Accounts []postgres.GetAccountsWithBalanceRow
|
||||
Budget postgres.Budget
|
||||
}{accounts, budget}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func (h *Handler) calculateBalances(c *gin.Context, budget postgres.Budget, firstOfNextMonth time.Time, firstOfMonth time.Time, categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, postgres.Numeric, error) {
|
||||
categoriesWithBalance := []CategoryWithBalance{}
|
||||
hiddenCategory := CategoryWithBalance{
|
||||
GetCategoriesRow: &postgres.GetCategoriesRow{
|
||||
Name: "",
|
||||
Group: "Hidden Categories",
|
||||
},
|
||||
Available: postgres.NewZeroNumeric(),
|
||||
AvailableLastMonth: postgres.NewZeroNumeric(),
|
||||
Activity: postgres.NewZeroNumeric(),
|
||||
Assigned: postgres.NewZeroNumeric(),
|
||||
}
|
||||
|
||||
moneyUsed := postgres.NewZeroNumeric()
|
||||
for i := range categories {
|
||||
cat := &categories[i]
|
||||
categoryWithBalance := CategoryWithBalance{
|
||||
GetCategoriesRow: cat,
|
||||
Available: postgres.NewZeroNumeric(),
|
||||
AvailableLastMonth: postgres.NewZeroNumeric(),
|
||||
Activity: postgres.NewZeroNumeric(),
|
||||
Assigned: postgres.NewZeroNumeric(),
|
||||
}
|
||||
for _, bal := range cumultativeBalances {
|
||||
if bal.CategoryID != cat.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
if !bal.Date.Before(firstOfNextMonth) {
|
||||
continue
|
||||
}
|
||||
|
||||
moneyUsed = moneyUsed.Sub(bal.Assignments)
|
||||
categoryWithBalance.Available = categoryWithBalance.Available.Add(bal.Assignments)
|
||||
categoryWithBalance.Available = categoryWithBalance.Available.Add(bal.Transactions)
|
||||
if !categoryWithBalance.Available.IsPositive() && bal.Date.Before(firstOfMonth) {
|
||||
moneyUsed = moneyUsed.Add(categoryWithBalance.Available)
|
||||
categoryWithBalance.Available = postgres.NewZeroNumeric()
|
||||
}
|
||||
|
||||
if bal.Date.Before(firstOfMonth) {
|
||||
categoryWithBalance.AvailableLastMonth = categoryWithBalance.Available
|
||||
} else if bal.Date.Before(firstOfNextMonth) {
|
||||
categoryWithBalance.Activity = bal.Transactions
|
||||
categoryWithBalance.Assigned = bal.Assignments
|
||||
}
|
||||
}
|
||||
|
||||
// do not show hidden categories
|
||||
if cat.Group == "Hidden Categories" {
|
||||
hiddenCategory.Available = hiddenCategory.Available.Add(categoryWithBalance.Available)
|
||||
hiddenCategory.AvailableLastMonth = hiddenCategory.AvailableLastMonth.Add(categoryWithBalance.AvailableLastMonth)
|
||||
hiddenCategory.Activity = hiddenCategory.Activity.Add(categoryWithBalance.Activity)
|
||||
hiddenCategory.Assigned = hiddenCategory.Assigned.Add(categoryWithBalance.Assigned)
|
||||
continue
|
||||
}
|
||||
|
||||
if cat.ID == budget.IncomeCategoryID {
|
||||
continue
|
||||
}
|
||||
|
||||
categoriesWithBalance = append(categoriesWithBalance, categoryWithBalance)
|
||||
}
|
||||
|
||||
categoriesWithBalance = append(categoriesWithBalance, hiddenCategory)
|
||||
|
||||
return categoriesWithBalance, moneyUsed, nil
|
||||
}
|
26
server/dashboard.go
Normal file
26
server/dashboard.go
Normal file
@ -0,0 +1,26 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer"
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) dashboard(c *gin.Context) {
|
||||
userID := c.MustGet("token").(budgeteer.Token).GetID()
|
||||
budgets, err := h.Service.GetBudgetsForUser(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
d := DashboardData{
|
||||
Budgets: budgets,
|
||||
}
|
||||
c.JSON(http.StatusOK, d)
|
||||
}
|
||||
|
||||
type DashboardData struct {
|
||||
Budgets []postgres.Budget
|
||||
}
|
112
server/http.go
Normal file
112
server/http.go
Normal file
@ -0,0 +1,112 @@
|
||||
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)
|
||||
err := router.Run(":1323")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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/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
|
||||
}
|
||||
|
||||
http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), file.(io.ReadSeeker))
|
||||
}
|
||||
|
||||
func enableCachingForStaticFiles() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if strings.HasPrefix(c.Request.RequestURI, "/static/") {
|
||||
c.Header("Cache-Control", "max-age=86400")
|
||||
}
|
||||
}
|
||||
}
|
36
server/json-date.go
Normal file
36
server/json-date.go
Normal file
@ -0,0 +1,36 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JSONDate time.Time
|
||||
|
||||
// Implement Marshaler and Unmarshaler interface
|
||||
func (j *JSONDate) UnmarshalJSON(b []byte) error {
|
||||
s := strings.Trim(string(b), "\"")
|
||||
t, err := time.Parse("2006-01-02", s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse date: %w", err)
|
||||
}
|
||||
*j = JSONDate(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j JSONDate) MarshalJSON() ([]byte, error) {
|
||||
result, err := json.Marshal(time.Time(j))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal date: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Maybe a Format function for printing your date
|
||||
func (j JSONDate) Format(s string) string {
|
||||
t := time.Time(j)
|
||||
return t.Format(s)
|
||||
}
|
158
server/session.go
Normal file
158
server/session.go
Normal file
@ -0,0 +1,158 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer"
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (h *Handler) verifyLogin(c *gin.Context) (budgeteer.Token, error) {
|
||||
tokenString := c.GetHeader("Authorization")
|
||||
if len(tokenString) < 8 {
|
||||
return nil, fmt.Errorf("no authorization header supplied")
|
||||
}
|
||||
|
||||
tokenString = tokenString[7:]
|
||||
token, err := h.TokenVerifier.VerifyToken(tokenString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("verify token '%s': %w", tokenString, err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (h *Handler) verifyLoginWithForbidden(c *gin.Context) {
|
||||
token, err := h.verifyLogin(c)
|
||||
if err != nil {
|
||||
//c.Header("WWW-Authenticate", "Bearer")
|
||||
c.AbortWithError(http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("token", token)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func (h *Handler) verifyLoginWithRedirect(c *gin.Context) {
|
||||
token, err := h.verifyLogin(c)
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("token", token)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
type loginInformation struct {
|
||||
Password string `json:"password"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
func (h *Handler) loginPost(c *gin.Context) {
|
||||
var login loginInformation
|
||||
err := c.BindJSON(&login)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.Service.GetUserByUsername(c.Request.Context(), login.User)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.CredentialsVerifier.Verify(login.Password, user.Password); err != nil {
|
||||
c.AbortWithError(http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.TokenVerifier.CreateToken(&user)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusUnauthorized, err)
|
||||
}
|
||||
|
||||
go h.UpdateLastLogin(user.ID)
|
||||
|
||||
budgets, err := h.Service.GetBudgetsForUser(c.Request.Context(), user.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, LoginResponse{token, user, budgets})
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string
|
||||
User postgres.User
|
||||
Budgets []postgres.Budget
|
||||
}
|
||||
|
||||
type registerInformation struct {
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (h *Handler) registerPost(c *gin.Context) {
|
||||
var register registerInformation
|
||||
err := c.BindJSON(®ister)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("parse body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if register.Email == "" || register.Password == "" || register.Name == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("e-mail, password and name are required"))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.Service.GetUserByUsername(c.Request.Context(), register.Email)
|
||||
if err == nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("email is already taken"))
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := h.CredentialsVerifier.Hash(register.Password)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
createUser := postgres.CreateUserParams{
|
||||
Name: register.Name,
|
||||
Password: hash,
|
||||
Email: register.Email,
|
||||
}
|
||||
user, err := h.Service.CreateUser(c.Request.Context(), createUser)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
token, err := h.TokenVerifier.CreateToken(&user)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusUnauthorized, err)
|
||||
}
|
||||
|
||||
go h.UpdateLastLogin(user.ID)
|
||||
|
||||
budgets, err := h.Service.GetBudgetsForUser(c.Request.Context(), user.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, LoginResponse{token, user, budgets})
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateLastLogin(userID uuid.UUID) {
|
||||
_, err := h.Service.UpdateLastLogin(context.Background(), userID)
|
||||
if err != nil {
|
||||
fmt.Printf("Error updating last login: %s", err)
|
||||
}
|
||||
}
|
90
server/transaction.go
Normal file
90
server/transaction.go
Normal file
@ -0,0 +1,90 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type NewTransactionPayload struct {
|
||||
Date JSONDate `json:"date"`
|
||||
Payee struct {
|
||||
ID uuid.NullUUID
|
||||
Name string
|
||||
} `json:"payee"`
|
||||
Category struct {
|
||||
ID uuid.NullUUID
|
||||
Name string
|
||||
} `json:"category"`
|
||||
Memo string `json:"memo"`
|
||||
Amount string `json:"amount"`
|
||||
BudgetID uuid.UUID `json:"budget_id"`
|
||||
AccountID uuid.UUID `json:"account_id"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
func (h *Handler) newTransaction(c *gin.Context) {
|
||||
var payload NewTransactionPayload
|
||||
err := c.BindJSON(&payload)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%v\n", payload)
|
||||
|
||||
amount := postgres.Numeric{}
|
||||
err = amount.Set(payload.Amount)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("amount: %w", err))
|
||||
}
|
||||
|
||||
/*transactionUUID, err := getNullUUIDFromParam(c, "transactionid")
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("parse transaction id: %w", err))
|
||||
return
|
||||
}*/
|
||||
|
||||
//if !transactionUUID.Valid {
|
||||
new := postgres.CreateTransactionParams{
|
||||
Memo: payload.Memo,
|
||||
Date: time.Time(payload.Date),
|
||||
Amount: amount,
|
||||
AccountID: payload.AccountID,
|
||||
PayeeID: payload.Payee.ID, //TODO handle new payee
|
||||
CategoryID: payload.Category.ID, //TODO handle new category
|
||||
Status: postgres.TransactionStatus(payload.State),
|
||||
}
|
||||
_, err = h.Service.CreateTransaction(c.Request.Context(), new)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
|
||||
}
|
||||
// }
|
||||
/*
|
||||
_, delete := c.GetPostForm("delete")
|
||||
if delete {
|
||||
err = h.Service.DeleteTransaction(c.Request.Context(), transactionUUID.UUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("delete transaction: %w", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
update := postgres.UpdateTransactionParams{
|
||||
ID: transactionUUID.UUID,
|
||||
Memo: payload.Memo,
|
||||
Date: time.Time(payload.Date),
|
||||
Amount: amount,
|
||||
AccountID: transactionAccountID,
|
||||
PayeeID: payload.Payee.ID, //TODO handle new payee
|
||||
CategoryID: payload.Category.ID, //TODO handle new category
|
||||
}
|
||||
err = h.Service.UpdateTransaction(c.Request.Context(), update)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("update transaction: %w", err))
|
||||
}*/
|
||||
}
|
66
server/ynab-import.go
Normal file
66
server/ynab-import.go
Normal file
@ -0,0 +1,66 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (h *Handler) importYNAB(c *gin.Context) {
|
||||
budgetID, succ := c.Params.Get("budgetid")
|
||||
if !succ {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("no budget_id specified"))
|
||||
return
|
||||
}
|
||||
|
||||
budgetUUID, err := uuid.Parse(budgetID)
|
||||
if !succ {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
ynab, err := postgres.NewYNABImport(c.Request.Context(), h.Service.Queries, budgetUUID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = ynab.ImportTransactions(c.Request.Context(), transactions)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
assignmentsFile, err := c.FormFile("assignments")
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
assignments, err := assignmentsFile.Open()
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = ynab.ImportAssignments(c.Request.Context(), assignments)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user