Merge pull request 'Add code linting to build' (#13) from linting into master
Reviewed-on: #13
This commit is contained in:
commit
a6eb2a2253
@ -8,7 +8,7 @@ steps:
|
||||
image: hub.javil.eu/budgeteer:dev
|
||||
pull: true
|
||||
commands:
|
||||
- task
|
||||
- task ci
|
||||
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
|
26
.golangci.yml
Normal file
26
.golangci.yml
Normal file
@ -0,0 +1,26 @@
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- golint
|
||||
- scopelint
|
||||
- maligned
|
||||
- interfacer
|
||||
- wsl
|
||||
- forbidigo
|
||||
- nlreturn
|
||||
- testpackage
|
||||
- ifshort
|
||||
- exhaustivestruct
|
||||
- gci # not working, shows errors on freshly formatted file
|
||||
- varnamelen
|
||||
linters-settings:
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
- io/ioutil.ReadFile
|
||||
- io.Copy(*bytes.Buffer)
|
||||
- (*github.com/gin-gonic/gin.Context).AbortWithError
|
||||
- (*github.com/gin-gonic/gin.Context).AbortWithError
|
||||
- io.Copy(os.Stdout)
|
||||
varnamelen:
|
||||
ignore-decls:
|
||||
- c *gin.Context
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -2,5 +2,8 @@
|
||||
"files.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/vendor": true
|
||||
},
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true,
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ pipeline:
|
||||
image: hub.javil.eu/budgeteer:dev
|
||||
pull: true
|
||||
commands:
|
||||
- task
|
||||
- task ci
|
||||
|
||||
docker:
|
||||
image: plugins/docker
|
||||
|
20
Taskfile.yml
20
Taskfile.yml
@ -33,12 +33,7 @@ tasks:
|
||||
sources:
|
||||
- ./go.mod
|
||||
- ./go.sum
|
||||
- ./cmd/budgeteer/*.go
|
||||
- ./*.go
|
||||
- ./config/*.go
|
||||
- ./http/*.go
|
||||
- ./jwt/*.go
|
||||
- ./postgres/*.go
|
||||
- ./**/*.go
|
||||
- ./web/dist/**/*
|
||||
- ./postgres/schema/*
|
||||
generates:
|
||||
@ -52,14 +47,25 @@ tasks:
|
||||
desc: Build budgeteer in dev mode
|
||||
deps: [gomod, sqlc]
|
||||
cmds:
|
||||
- go vet
|
||||
- go fmt
|
||||
- golangci-lint run
|
||||
- task: build
|
||||
|
||||
build-prod:
|
||||
desc: Build budgeteer in prod mode
|
||||
deps: [gomod, sqlc, frontend]
|
||||
cmds:
|
||||
- go vet
|
||||
- go fmt
|
||||
- golangci-lint run
|
||||
- task: build
|
||||
|
||||
ci:
|
||||
desc: Run CI build
|
||||
cmds:
|
||||
- task: build-prod
|
||||
|
||||
frontend:
|
||||
desc: Build vue frontend
|
||||
dir: web
|
||||
@ -85,6 +91,8 @@ tasks:
|
||||
desc: Build budgeeter:dev
|
||||
sources:
|
||||
- ./docker/Dockerfile
|
||||
- ./docker/build.sh
|
||||
- ./web/package.json
|
||||
cmds:
|
||||
- docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile
|
||||
- docker push {{.IMAGE_NAME}}:dev
|
||||
|
@ -1,23 +1,30 @@
|
||||
package bcrypt
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// Verifier verifys passwords using Bcrypt
|
||||
type Verifier struct {
|
||||
cost int
|
||||
}
|
||||
|
||||
// Verify verifys a Password
|
||||
func (bv *Verifier) Verify(password string, hashOnDb string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashOnDb), []byte(password))
|
||||
}
|
||||
|
||||
// Hash calculates a hash to be stored on the database
|
||||
func (bv *Verifier) Hash(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bv.cost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(hash[:]), nil
|
||||
}
|
||||
package bcrypt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Verifier verifys passwords using Bcrypt.
|
||||
type Verifier struct{}
|
||||
|
||||
// Verify verifys a Password.
|
||||
func (bv *Verifier) Verify(password string, hashOnDB string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashOnDB), []byte(password))
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify password: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hash calculates a hash to be stored on the database.
|
||||
func (bv *Verifier) Hash(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
|
||||
return string(hash), nil
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/bcrypt"
|
||||
"git.javil.eu/jacob1123/budgeteer/config"
|
||||
"git.javil.eu/jacob1123/budgeteer/http"
|
||||
"git.javil.eu/jacob1123/budgeteer/jwt"
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"git.javil.eu/jacob1123/budgeteer/server"
|
||||
"git.javil.eu/jacob1123/budgeteer/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -16,16 +19,24 @@ func main() {
|
||||
log.Fatalf("Could not load config: %v", err)
|
||||
}
|
||||
|
||||
q, err := postgres.Connect("pgx", cfg.DatabaseConnection)
|
||||
queries, err := postgres.Connect("pgx", cfg.DatabaseConnection)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed connecting to DB: %v", err)
|
||||
}
|
||||
|
||||
h := &http.Handler{
|
||||
Service: q,
|
||||
TokenVerifier: &jwt.TokenVerifier{},
|
||||
CredentialsVerifier: &bcrypt.Verifier{},
|
||||
static, err := fs.Sub(web.Static, "dist")
|
||||
if err != nil {
|
||||
panic("couldn't open static files")
|
||||
}
|
||||
|
||||
h.Serve()
|
||||
handler := &server.Handler{
|
||||
Service: queries,
|
||||
TokenVerifier: &jwt.TokenVerifier{
|
||||
Secret: cfg.SessionSecret,
|
||||
},
|
||||
CredentialsVerifier: &bcrypt.Verifier{},
|
||||
StaticFS: http.FS(static),
|
||||
}
|
||||
|
||||
handler.Serve()
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Config contains all needed configurations
|
||||
type Config struct {
|
||||
DatabaseConnection string
|
||||
}
|
||||
|
||||
// LoadConfig from path
|
||||
func LoadConfig() (*Config, error) {
|
||||
configuration := Config{
|
||||
DatabaseConnection: os.Getenv("BUDGETEER_DB"),
|
||||
}
|
||||
|
||||
return &configuration, nil
|
||||
}
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Config contains all needed configurations.
|
||||
type Config struct {
|
||||
DatabaseConnection string
|
||||
SessionSecret string
|
||||
}
|
||||
|
||||
// LoadConfig from path.
|
||||
func LoadConfig() (*Config, error) {
|
||||
configuration := Config{
|
||||
DatabaseConnection: os.Getenv("BUDGETEER_DB"),
|
||||
SessionSecret: os.Getenv("BUDGETEER_SESSION_SECRET"),
|
||||
}
|
||||
|
||||
return &configuration, nil
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ services:
|
||||
- ~/.cache:/.cache
|
||||
environment:
|
||||
BUDGETEER_DB: postgres://budgeteer:budgeteer@db:5432/budgeteer
|
||||
BUDGETEER_SESSION_SECRET: random string for JWT authorization
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
|
@ -2,15 +2,16 @@ FROM alpine as godeps
|
||||
RUN apk add go
|
||||
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
|
||||
RUN go install github.com/go-task/task/v3/cmd/task@latest
|
||||
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
FROM alpine
|
||||
RUN apk add go
|
||||
RUN apk add nodejs yarn bash curl git git-perl tmux
|
||||
ADD docker/build.sh /
|
||||
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /usr/local/bin/
|
||||
RUN yarn global add @vue/cli
|
||||
ENV PATH="/root/.yarn/bin/:${PATH}"
|
||||
WORKDIR /src
|
||||
ADD web/package.json /src/web/
|
||||
RUN yarn
|
||||
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/
|
||||
CMD /build.sh
|
||||
|
@ -1,89 +0,0 @@
|
||||
package http
|
||||
|
||||
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.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%v\n", payload)
|
||||
|
||||
amount := postgres.Numeric{}
|
||||
amount.Set(payload.Amount)
|
||||
|
||||
/*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))
|
||||
}
|
||||
|
||||
return
|
||||
// }
|
||||
/*
|
||||
_, 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))
|
||||
}*/
|
||||
}
|
56
http/util.go
56
http/util.go
@ -1,56 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func getUUID(c *gin.Context, name string) (uuid.UUID, error) {
|
||||
value, succ := c.GetPostForm(name)
|
||||
if !succ {
|
||||
return uuid.UUID{}, fmt.Errorf("not set")
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(value)
|
||||
if err != nil {
|
||||
return uuid.UUID{}, fmt.Errorf("not a valid uuid: %w", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func getNullUUIDFromParam(c *gin.Context, name string) (uuid.NullUUID, error) {
|
||||
value := c.Param(name)
|
||||
if value == "" {
|
||||
return uuid.NullUUID{}, nil
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(value)
|
||||
if err != nil {
|
||||
return uuid.NullUUID{}, fmt.Errorf("not a valid uuid: %w", err)
|
||||
}
|
||||
|
||||
return uuid.NullUUID{
|
||||
UUID: id,
|
||||
Valid: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getNullUUIDFromForm(c *gin.Context, name string) (uuid.NullUUID, error) {
|
||||
value, succ := c.GetPostForm(name)
|
||||
if !succ || value == "" {
|
||||
return uuid.NullUUID{}, nil
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(value)
|
||||
if err != nil {
|
||||
return uuid.NullUUID{}, fmt.Errorf("not a valid uuid: %w", err)
|
||||
}
|
||||
|
||||
return uuid.NullUUID{
|
||||
UUID: id,
|
||||
Valid: true,
|
||||
}, nil
|
||||
}
|
34
jwt/login.go
34
jwt/login.go
@ -10,11 +10,12 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TokenVerifier verifies Tokens
|
||||
// TokenVerifier verifies Tokens.
|
||||
type TokenVerifier struct {
|
||||
Secret string
|
||||
}
|
||||
|
||||
// Token contains everything to authenticate a user
|
||||
// Token contains everything to authenticate a user.
|
||||
type Token struct {
|
||||
username string
|
||||
name string
|
||||
@ -24,10 +25,9 @@ type Token struct {
|
||||
|
||||
const (
|
||||
expiration = 72
|
||||
secret = "uditapbzuditagscwxuqdflgzpbu´ßiaefnlmzeßtrubiadern"
|
||||
)
|
||||
|
||||
// CreateToken creates a new token from username and name
|
||||
// CreateToken creates a new token from username and name.
|
||||
func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"usr": user.Email,
|
||||
@ -37,21 +37,27 @@ func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
|
||||
})
|
||||
|
||||
// Generate encoded token and send it as response.
|
||||
t, err := token.SignedString([]byte(secret))
|
||||
t, err := token.SignedString([]byte(tv.Secret))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("create token: %w", err)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// VerifyToken verifys a given string-token
|
||||
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) {
|
||||
var (
|
||||
ErrUnexpectedSigningMethod = fmt.Errorf("unexpected signing method")
|
||||
ErrInvalidToken = fmt.Errorf("token is invalid")
|
||||
ErrTokenExpired = fmt.Errorf("token has expired")
|
||||
)
|
||||
|
||||
// VerifyToken verifys a given string-token.
|
||||
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod)
|
||||
}
|
||||
return []byte(secret), nil
|
||||
return []byte(tv.Secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse jwt: %w", err)
|
||||
@ -62,7 +68,7 @@ func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error
|
||||
return nil, fmt.Errorf("verify jwt: %w", err)
|
||||
}
|
||||
|
||||
tkn := &Token{
|
||||
tkn := &Token{ //nolint:forcetypeassert
|
||||
username: claims["usr"].(string),
|
||||
name: claims["name"].(string),
|
||||
expiry: claims["exp"].(float64),
|
||||
@ -73,16 +79,16 @@ func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error
|
||||
|
||||
func verifyToken(token *jwt.Token) (jwt.MapClaims, error) {
|
||||
if !token.Valid {
|
||||
return nil, fmt.Errorf("Token is not valid")
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Claims are not of Type MapClaims")
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
|
||||
return nil, fmt.Errorf("Claims have expired")
|
||||
return nil, ErrTokenExpired
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
|
@ -8,11 +8,15 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// NewBudget creates a budget and adds it to the current user
|
||||
// NewBudget creates a budget and adds it to the current user.
|
||||
func (s *Database) NewBudget(context context.Context, name string, userID uuid.UUID) (*Budget, error) {
|
||||
tx, err := s.BeginTx(context, &sql.TxOptions{})
|
||||
q := s.WithTx(tx)
|
||||
budget, err := q.CreateBudget(context, CreateBudgetParams{
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin transaction: %w", err)
|
||||
}
|
||||
|
||||
transaction := s.WithTx(tx)
|
||||
budget, err := transaction.CreateBudget(context, CreateBudgetParams{
|
||||
Name: name,
|
||||
IncomeCategoryID: uuid.New(),
|
||||
})
|
||||
@ -21,12 +25,12 @@ func (s *Database) NewBudget(context context.Context, name string, userID uuid.U
|
||||
}
|
||||
|
||||
ub := LinkBudgetToUserParams{UserID: userID, BudgetID: budget.ID}
|
||||
_, err = q.LinkBudgetToUser(context, ub)
|
||||
_, err = transaction.LinkBudgetToUser(context, ub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("link budget to user: %w", err)
|
||||
}
|
||||
|
||||
group, err := q.CreateCategoryGroup(context, CreateCategoryGroupParams{
|
||||
group, err := transaction.CreateCategoryGroup(context, CreateCategoryGroupParams{
|
||||
Name: "Inflow",
|
||||
BudgetID: budget.ID,
|
||||
})
|
||||
@ -34,7 +38,7 @@ func (s *Database) NewBudget(context context.Context, name string, userID uuid.U
|
||||
return nil, fmt.Errorf("create inflow category_group: %w", err)
|
||||
}
|
||||
|
||||
cat, err := q.CreateCategory(context, CreateCategoryParams{
|
||||
cat, err := transaction.CreateCategory(context, CreateCategoryParams{
|
||||
Name: "Ready to Assign",
|
||||
CategoryGroupID: group.ID,
|
||||
})
|
||||
@ -42,7 +46,7 @@ func (s *Database) NewBudget(context context.Context, name string, userID uuid.U
|
||||
return nil, fmt.Errorf("create ready to assign category: %w", err)
|
||||
}
|
||||
|
||||
err = q.SetInflowCategory(context, SetInflowCategoryParams{
|
||||
err = transaction.SetInflowCategory(context, SetInflowCategoryParams{
|
||||
IncomeCategoryID: cat.ID,
|
||||
ID: budget.ID,
|
||||
})
|
||||
@ -50,7 +54,10 @@ func (s *Database) NewBudget(context context.Context, name string, userID uuid.U
|
||||
return nil, fmt.Errorf("set inflow category: %w", err)
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
return &budget, nil
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
_ "github.com/jackc/pgx/v4/stdlib" // needed for pg connection
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
@ -17,7 +17,7 @@ type Database struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
// Connect to a database
|
||||
// Connect connects to a database.
|
||||
func Connect(typ string, connString string) (*Database, error) {
|
||||
conn, err := sql.Open(typ, connString)
|
||||
if err != nil {
|
||||
|
@ -45,7 +45,7 @@ func (n Numeric) IsZero() bool {
|
||||
|
||||
func (n Numeric) MatchExp(exp int32) Numeric {
|
||||
diffExp := n.Exp - exp
|
||||
factor := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(diffExp)), nil)
|
||||
factor := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(diffExp)), nil) //nolint:gomnd
|
||||
return Numeric{pgtype.Numeric{
|
||||
Exp: exp,
|
||||
Int: big.NewInt(0).Mul(n.Int, factor),
|
||||
@ -54,13 +54,13 @@ func (n Numeric) MatchExp(exp int32) Numeric {
|
||||
}}
|
||||
}
|
||||
|
||||
func (n Numeric) Sub(o Numeric) Numeric {
|
||||
func (n Numeric) Sub(other Numeric) Numeric {
|
||||
left := n
|
||||
right := o
|
||||
if n.Exp < o.Exp {
|
||||
right = o.MatchExp(n.Exp)
|
||||
} else if n.Exp > o.Exp {
|
||||
left = n.MatchExp(o.Exp)
|
||||
right := other
|
||||
if n.Exp < other.Exp {
|
||||
right = other.MatchExp(n.Exp)
|
||||
} else if n.Exp > other.Exp {
|
||||
left = n.MatchExp(other.Exp)
|
||||
}
|
||||
|
||||
if left.Exp == right.Exp {
|
||||
@ -72,13 +72,14 @@ func (n Numeric) Sub(o Numeric) Numeric {
|
||||
|
||||
panic("Cannot subtract with different exponents")
|
||||
}
|
||||
func (n Numeric) Add(o Numeric) Numeric {
|
||||
|
||||
func (n Numeric) Add(other Numeric) Numeric {
|
||||
left := n
|
||||
right := o
|
||||
if n.Exp < o.Exp {
|
||||
right = o.MatchExp(n.Exp)
|
||||
} else if n.Exp > o.Exp {
|
||||
left = n.MatchExp(o.Exp)
|
||||
right := other
|
||||
if n.Exp < other.Exp {
|
||||
right = other.MatchExp(n.Exp)
|
||||
} else if n.Exp > other.Exp {
|
||||
left = n.MatchExp(other.Exp)
|
||||
}
|
||||
|
||||
if left.Exp == right.Exp {
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
)
|
||||
|
||||
type YNABImport struct {
|
||||
Context context.Context
|
||||
accounts []Account
|
||||
payees []Payee
|
||||
categories []GetCategoriesRow
|
||||
@ -22,73 +21,70 @@ type YNABImport struct {
|
||||
budgetID uuid.UUID
|
||||
}
|
||||
|
||||
func NewYNABImport(context context.Context, q *Queries, budgetID uuid.UUID) (*YNABImport, error) {
|
||||
accounts, err := q.GetAccounts(context, budgetID)
|
||||
func NewYNABImport(context context.Context, queries *Queries, budgetID uuid.UUID) (*YNABImport, error) {
|
||||
accounts, err := queries.GetAccounts(context, budgetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payees, err := q.GetPayees(context, budgetID)
|
||||
payees, err := queries.GetPayees(context, budgetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
categories, err := q.GetCategories(context, budgetID)
|
||||
categories, err := queries.GetCategories(context, budgetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
categoryGroups, err := q.GetCategoryGroups(context, budgetID)
|
||||
categoryGroups, err := queries.GetCategoryGroups(context, budgetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &YNABImport{
|
||||
Context: context,
|
||||
accounts: accounts,
|
||||
payees: payees,
|
||||
categories: categories,
|
||||
categoryGroups: categoryGroups,
|
||||
queries: q,
|
||||
queries: queries,
|
||||
budgetID: budgetID,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// ImportAssignments expects a TSV-file as exported by YNAB in the following format:
|
||||
//"Month" "Category Group/Category" "Category Group" "Category" "Budgeted" "Activity" "Available"
|
||||
//"Apr 2019" "Income: Next Month" "Income" "Next Month" 0,00€ 0,00€ 0,00€
|
||||
// "Month" "Category Group/Category" "Category Group" "Category" "Budgeted" "Activity" "Available"
|
||||
// "Apr 2019" "Income: Next Month" "Income" "Next Month" 0,00€ 0,00€ 0,00€
|
||||
//
|
||||
// Activity and Available are not imported, since they are determined by the transactions and historic assignments
|
||||
func (ynab *YNABImport) ImportAssignments(r io.Reader) error {
|
||||
// Activity and Available are not imported, since they are determined by the transactions and historic assignments.
|
||||
func (ynab *YNABImport) ImportAssignments(context context.Context, r io.Reader) error {
|
||||
csv := csv.NewReader(r)
|
||||
csv.Comma = '\t'
|
||||
csv.LazyQuotes = true
|
||||
|
||||
csvData, err := csv.ReadAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read from tsv: %w", err)
|
||||
return fmt.Errorf("read from tsv: %w", err)
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, record := range csvData[1:] {
|
||||
|
||||
dateString := record[0]
|
||||
date, err := time.Parse("Jan 2006", dateString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse date %s: %w", dateString, err)
|
||||
return fmt.Errorf("parse date %s: %w", dateString, err)
|
||||
}
|
||||
|
||||
categoryGroup, categoryName := record[2], record[3] //also in 1 joined by :
|
||||
category, err := ynab.GetCategory(categoryGroup, categoryName)
|
||||
categoryGroup, categoryName := record[2], record[3] // also in 1 joined by :
|
||||
category, err := ynab.GetCategory(context, categoryGroup, categoryName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err)
|
||||
return fmt.Errorf("get category %s/%s: %w", categoryGroup, categoryName, err)
|
||||
}
|
||||
|
||||
amountString := record[4]
|
||||
amount, err := GetAmount(amountString, "0,00€")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse amount %s: %w", amountString, err)
|
||||
return fmt.Errorf("parse amount %s: %w", amountString, err)
|
||||
}
|
||||
|
||||
if amount.Int.Int64() == 0 {
|
||||
@ -100,9 +96,9 @@ func (ynab *YNABImport) ImportAssignments(r io.Reader) error {
|
||||
CategoryID: category.UUID,
|
||||
Amount: amount,
|
||||
}
|
||||
_, err = ynab.queries.CreateAssignment(ynab.Context, assignment)
|
||||
_, err = ynab.queries.CreateAssignment(context, assignment)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save assignment %v: %w", assignment, err)
|
||||
return fmt.Errorf("save assignment %v: %w", assignment, err)
|
||||
}
|
||||
|
||||
count++
|
||||
@ -120,150 +116,183 @@ type Transfer struct {
|
||||
ToAccount string
|
||||
}
|
||||
|
||||
// ImportTransactions expects a TSV-file as exported by YNAB in the following format:
|
||||
|
||||
func (ynab *YNABImport) ImportTransactions(r io.Reader) error {
|
||||
// ImportTransactions expects a TSV-file as exported by YNAB.
|
||||
func (ynab *YNABImport) ImportTransactions(context context.Context, r io.Reader) error {
|
||||
csv := csv.NewReader(r)
|
||||
csv.Comma = '\t'
|
||||
csv.LazyQuotes = true
|
||||
|
||||
csvData, err := csv.ReadAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read from tsv: %w", err)
|
||||
return fmt.Errorf("read from tsv: %w", err)
|
||||
}
|
||||
|
||||
var openTransfers []Transfer
|
||||
|
||||
count := 0
|
||||
for _, record := range csvData[1:] {
|
||||
accountName := record[0]
|
||||
account, err := ynab.GetAccount(accountName)
|
||||
transaction, err := ynab.GetTransaction(context, record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get account %s: %w", accountName, err)
|
||||
}
|
||||
|
||||
//flag := record[1]
|
||||
|
||||
dateString := record[2]
|
||||
date, err := time.Parse("02.01.2006", dateString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse date %s: %w", dateString, err)
|
||||
}
|
||||
|
||||
categoryGroup, categoryName := record[5], record[6] //also in 4 joined by :
|
||||
category, err := ynab.GetCategory(categoryGroup, categoryName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err)
|
||||
}
|
||||
|
||||
memo := record[7]
|
||||
|
||||
outflow := record[8]
|
||||
inflow := record[9]
|
||||
amount, err := GetAmount(inflow, outflow)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse amount from (%s/%s): %w", inflow, outflow, err)
|
||||
}
|
||||
|
||||
statusEnum := TransactionStatusUncleared
|
||||
status := record[10]
|
||||
switch status {
|
||||
case "Cleared":
|
||||
statusEnum = TransactionStatusCleared
|
||||
case "Reconciled":
|
||||
statusEnum = TransactionStatusReconciled
|
||||
case "Uncleared":
|
||||
}
|
||||
|
||||
transaction := CreateTransactionParams{
|
||||
Date: date,
|
||||
Memo: memo,
|
||||
AccountID: account.ID,
|
||||
CategoryID: category,
|
||||
Amount: amount,
|
||||
Status: statusEnum,
|
||||
return err
|
||||
}
|
||||
|
||||
payeeName := record[3]
|
||||
// Transaction is a transfer
|
||||
if strings.HasPrefix(payeeName, "Transfer : ") {
|
||||
// Transaction is a transfer to
|
||||
transferToAccountName := payeeName[11:]
|
||||
transferToAccount, err := ynab.GetAccount(transferToAccountName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not get transfer account %s: %w", transferToAccountName, err)
|
||||
}
|
||||
|
||||
transfer := Transfer{
|
||||
transaction,
|
||||
transferToAccount,
|
||||
accountName,
|
||||
transferToAccountName,
|
||||
}
|
||||
|
||||
found := false
|
||||
for i, openTransfer := range openTransfers {
|
||||
if openTransfer.TransferToAccount.ID != transfer.AccountID {
|
||||
continue
|
||||
}
|
||||
if openTransfer.AccountID != transfer.TransferToAccount.ID {
|
||||
continue
|
||||
}
|
||||
if openTransfer.Amount.GetFloat64() != -1*transfer.Amount.GetFloat64() {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("Matched transfers from %s to %s over %f\n", account.Name, transferToAccount.Name, amount.GetFloat64())
|
||||
openTransfers[i] = openTransfers[len(openTransfers)-1]
|
||||
openTransfers = openTransfers[:len(openTransfers)-1]
|
||||
found = true
|
||||
|
||||
groupID := uuid.New()
|
||||
transfer.GroupID = uuid.NullUUID{UUID: groupID, Valid: true}
|
||||
openTransfer.GroupID = uuid.NullUUID{UUID: groupID, Valid: true}
|
||||
|
||||
_, err = ynab.queries.CreateTransaction(ynab.Context, transfer.CreateTransactionParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save transaction %v: %w", transfer.CreateTransactionParams, err)
|
||||
}
|
||||
_, err = ynab.queries.CreateTransaction(ynab.Context, openTransfer.CreateTransactionParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save transaction %v: %w", openTransfer.CreateTransactionParams, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if !found {
|
||||
openTransfers = append(openTransfers, transfer)
|
||||
}
|
||||
err = ynab.ImportTransferTransaction(context, payeeName, transaction.CreateTransactionParams,
|
||||
&openTransfers, transaction.Account, transaction.Amount)
|
||||
} else {
|
||||
payeeID, err := ynab.GetPayee(payeeName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get payee %s: %w", payeeName, err)
|
||||
}
|
||||
transaction.PayeeID = payeeID
|
||||
|
||||
_, err = ynab.queries.CreateTransaction(ynab.Context, transaction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save transaction %v: %w", transaction, err)
|
||||
}
|
||||
err = ynab.ImportRegularTransaction(context, payeeName, transaction.CreateTransactionParams)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
for _, openTransfer := range openTransfers {
|
||||
fmt.Printf("Saving unmatched transfer from %s to %s on %s over %f as regular transaction\n", openTransfer.FromAccount, openTransfer.ToAccount, openTransfer.Date, openTransfer.Amount.GetFloat64())
|
||||
_, err = ynab.queries.CreateTransaction(ynab.Context, openTransfer.CreateTransactionParams)
|
||||
fmt.Printf("Saving unmatched transfer from %s to %s on %s over %f as regular transaction\n",
|
||||
openTransfer.FromAccount, openTransfer.ToAccount, openTransfer.Date, openTransfer.Amount.GetFloat64())
|
||||
_, err = ynab.queries.CreateTransaction(context, openTransfer.CreateTransactionParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save transaction %v: %w", openTransfer.CreateTransactionParams, err)
|
||||
return fmt.Errorf("save transaction %v: %w", openTransfer.CreateTransactionParams, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fmt.Printf("Imported %d transactions\n", count)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type NewTransaction struct {
|
||||
CreateTransactionParams
|
||||
Account *Account
|
||||
}
|
||||
|
||||
func (ynab *YNABImport) GetTransaction(context context.Context, record []string) (NewTransaction, error) {
|
||||
accountName := record[0]
|
||||
account, err := ynab.GetAccount(context, accountName)
|
||||
if err != nil {
|
||||
return NewTransaction{}, fmt.Errorf("get account %s: %w", accountName, err)
|
||||
}
|
||||
|
||||
// flag := record[1]
|
||||
|
||||
dateString := record[2]
|
||||
date, err := time.Parse("02.01.2006", dateString)
|
||||
if err != nil {
|
||||
return NewTransaction{}, fmt.Errorf("parse date %s: %w", dateString, err)
|
||||
}
|
||||
|
||||
categoryGroup, categoryName := record[5], record[6] // also in 4 joined by :
|
||||
category, err := ynab.GetCategory(context, categoryGroup, categoryName)
|
||||
if err != nil {
|
||||
return NewTransaction{}, fmt.Errorf("get category %s/%s: %w", categoryGroup, categoryName, err)
|
||||
}
|
||||
|
||||
memo := record[7]
|
||||
|
||||
outflow := record[8]
|
||||
inflow := record[9]
|
||||
amount, err := GetAmount(inflow, outflow)
|
||||
if err != nil {
|
||||
return NewTransaction{}, fmt.Errorf("parse amount from (%s/%s): %w", inflow, outflow, err)
|
||||
}
|
||||
|
||||
statusEnum := TransactionStatusUncleared
|
||||
status := record[10]
|
||||
switch status {
|
||||
case "Cleared":
|
||||
statusEnum = TransactionStatusCleared
|
||||
case "Reconciled":
|
||||
statusEnum = TransactionStatusReconciled
|
||||
case "Uncleared":
|
||||
}
|
||||
|
||||
return NewTransaction{
|
||||
CreateTransactionParams: CreateTransactionParams{
|
||||
Date: date,
|
||||
Memo: memo,
|
||||
AccountID: account.ID,
|
||||
CategoryID: category,
|
||||
Amount: amount,
|
||||
Status: statusEnum,
|
||||
},
|
||||
Account: account,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ynab *YNABImport) ImportRegularTransaction(context context.Context, payeeName string,
|
||||
transaction CreateTransactionParams) error {
|
||||
payeeID, err := ynab.GetPayee(context, payeeName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get payee %s: %w", payeeName, err)
|
||||
}
|
||||
transaction.PayeeID = payeeID
|
||||
|
||||
_, err = ynab.queries.CreateTransaction(context, transaction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save transaction %v: %w", transaction, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ynab *YNABImport) ImportTransferTransaction(context context.Context, payeeName string,
|
||||
transaction CreateTransactionParams, openTransfers *[]Transfer,
|
||||
account *Account, amount Numeric) error {
|
||||
transferToAccountName := payeeName[11:]
|
||||
transferToAccount, err := ynab.GetAccount(context, transferToAccountName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get transfer account %s: %w", transferToAccountName, err)
|
||||
}
|
||||
|
||||
transfer := Transfer{
|
||||
transaction,
|
||||
transferToAccount,
|
||||
account.Name,
|
||||
transferToAccountName,
|
||||
}
|
||||
|
||||
found := false
|
||||
for i, openTransfer := range *openTransfers {
|
||||
if openTransfer.TransferToAccount.ID != transfer.AccountID {
|
||||
continue
|
||||
}
|
||||
if openTransfer.AccountID != transfer.TransferToAccount.ID {
|
||||
continue
|
||||
}
|
||||
if openTransfer.Amount.GetFloat64() != -1*transfer.Amount.GetFloat64() {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("Matched transfers from %s to %s over %f\n", account.Name, transferToAccount.Name, amount.GetFloat64())
|
||||
transfers := *openTransfers
|
||||
transfers[i] = transfers[len(transfers)-1]
|
||||
*openTransfers = transfers[:len(transfers)-1]
|
||||
found = true
|
||||
|
||||
groupID := uuid.New()
|
||||
transfer.GroupID = uuid.NullUUID{UUID: groupID, Valid: true}
|
||||
openTransfer.GroupID = uuid.NullUUID{UUID: groupID, Valid: true}
|
||||
|
||||
_, err = ynab.queries.CreateTransaction(context, transfer.CreateTransactionParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save transaction %v: %w", transfer.CreateTransactionParams, err)
|
||||
}
|
||||
_, err = ynab.queries.CreateTransaction(context, openTransfer.CreateTransactionParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save transaction %v: %w", openTransfer.CreateTransactionParams, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if !found {
|
||||
*openTransfers = append(*openTransfers, transfer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func trimLastChar(s string) string {
|
||||
r, size := utf8.DecodeLastRuneInString(s)
|
||||
if r == utf8.RuneError && (size == 0 || size == 1) {
|
||||
@ -280,7 +309,7 @@ func GetAmount(inflow string, outflow string) (Numeric, error) {
|
||||
num := Numeric{}
|
||||
err := num.Set(inflow)
|
||||
if err != nil {
|
||||
return num, fmt.Errorf("Could not parse inflow %s: %w", inflow, err)
|
||||
return num, fmt.Errorf("parse inflow %s: %w", inflow, err)
|
||||
}
|
||||
|
||||
// if inflow is zero, use outflow
|
||||
@ -290,19 +319,19 @@ func GetAmount(inflow string, outflow string) (Numeric, error) {
|
||||
|
||||
err = num.Set("-" + outflow)
|
||||
if err != nil {
|
||||
return num, fmt.Errorf("Could not parse outflow %s: %w", inflow, err)
|
||||
return num, fmt.Errorf("parse outflow %s: %w", inflow, err)
|
||||
}
|
||||
return num, nil
|
||||
}
|
||||
|
||||
func (ynab *YNABImport) GetAccount(name string) (*Account, error) {
|
||||
func (ynab *YNABImport) GetAccount(context context.Context, name string) (*Account, error) {
|
||||
for _, acc := range ynab.accounts {
|
||||
if acc.Name == name {
|
||||
return &acc, nil
|
||||
}
|
||||
}
|
||||
|
||||
account, err := ynab.queries.CreateAccount(ynab.Context, CreateAccountParams{Name: name, BudgetID: ynab.budgetID})
|
||||
account, err := ynab.queries.CreateAccount(context, CreateAccountParams{Name: name, BudgetID: ynab.budgetID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -311,7 +340,7 @@ func (ynab *YNABImport) GetAccount(name string) (*Account, error) {
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (ynab *YNABImport) GetPayee(name string) (uuid.NullUUID, error) {
|
||||
func (ynab *YNABImport) GetPayee(context context.Context, name string) (uuid.NullUUID, error) {
|
||||
if name == "" {
|
||||
return uuid.NullUUID{}, nil
|
||||
}
|
||||
@ -322,7 +351,7 @@ func (ynab *YNABImport) GetPayee(name string) (uuid.NullUUID, error) {
|
||||
}
|
||||
}
|
||||
|
||||
payee, err := ynab.queries.CreatePayee(ynab.Context, CreatePayeeParams{Name: name, BudgetID: ynab.budgetID})
|
||||
payee, err := ynab.queries.CreatePayee(context, CreatePayeeParams{Name: name, BudgetID: ynab.budgetID})
|
||||
if err != nil {
|
||||
return uuid.NullUUID{}, err
|
||||
}
|
||||
@ -331,7 +360,7 @@ func (ynab *YNABImport) GetPayee(name string) (uuid.NullUUID, error) {
|
||||
return uuid.NullUUID{UUID: payee.ID, Valid: true}, nil
|
||||
}
|
||||
|
||||
func (ynab *YNABImport) GetCategory(group string, name string) (uuid.NullUUID, error) {
|
||||
func (ynab *YNABImport) GetCategory(context context.Context, group string, name string) (uuid.NullUUID, error) { //nolint
|
||||
if group == "" || name == "" {
|
||||
return uuid.NullUUID{}, nil
|
||||
}
|
||||
@ -342,32 +371,25 @@ func (ynab *YNABImport) GetCategory(group string, name string) (uuid.NullUUID, e
|
||||
}
|
||||
}
|
||||
|
||||
for _, categoryGroup := range ynab.categoryGroups {
|
||||
if categoryGroup.Name == group {
|
||||
createCategory := CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID}
|
||||
category, err := ynab.queries.CreateCategory(ynab.Context, createCategory)
|
||||
if err != nil {
|
||||
return uuid.NullUUID{}, err
|
||||
}
|
||||
|
||||
getCategory := GetCategoriesRow{
|
||||
ID: category.ID,
|
||||
CategoryGroupID: category.CategoryGroupID,
|
||||
Name: category.Name,
|
||||
Group: categoryGroup.Name,
|
||||
}
|
||||
ynab.categories = append(ynab.categories, getCategory)
|
||||
return uuid.NullUUID{UUID: category.ID, Valid: true}, nil
|
||||
var categoryGroup CategoryGroup
|
||||
for _, existingGroup := range ynab.categoryGroups {
|
||||
if existingGroup.Name == group {
|
||||
categoryGroup = existingGroup
|
||||
}
|
||||
}
|
||||
|
||||
categoryGroup, err := ynab.queries.CreateCategoryGroup(ynab.Context, CreateCategoryGroupParams{Name: group, BudgetID: ynab.budgetID})
|
||||
if err != nil {
|
||||
return uuid.NullUUID{}, err
|
||||
if categoryGroup.Name == "" {
|
||||
newGroup := CreateCategoryGroupParams{Name: group, BudgetID: ynab.budgetID}
|
||||
var err error
|
||||
categoryGroup, err = ynab.queries.CreateCategoryGroup(context, newGroup)
|
||||
if err != nil {
|
||||
return uuid.NullUUID{}, err
|
||||
}
|
||||
ynab.categoryGroups = append(ynab.categoryGroups, categoryGroup)
|
||||
}
|
||||
ynab.categoryGroups = append(ynab.categoryGroups, categoryGroup)
|
||||
|
||||
category, err := ynab.queries.CreateCategory(ynab.Context, CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID})
|
||||
newCategory := CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID}
|
||||
category, err := ynab.queries.CreateCategory(context, newCategory)
|
||||
if err != nil {
|
||||
return uuid.NullUUID{}, err
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -10,47 +10,53 @@ import (
|
||||
"git.javil.eu/jacob1123/budgeteer/bcrypt"
|
||||
"git.javil.eu/jacob1123/budgeteer/jwt"
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
txdb "github.com/DATA-DOG/go-txdb"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func init() {
|
||||
func init() { //nolint:gochecknoinits
|
||||
txdb.Register("pgtx", "pgx", "postgres://budgeteer_test:budgeteer_test@localhost:5432/budgeteer_test")
|
||||
}
|
||||
|
||||
func TestListTimezonesHandler(t *testing.T) {
|
||||
db, err := postgres.Connect("pgtx", "example")
|
||||
func TestRegisterUser(t *testing.T) { //nolint:funlen
|
||||
t.Parallel()
|
||||
database, err := postgres.Connect("pgtx", "example")
|
||||
if err != nil {
|
||||
t.Errorf("could not connect to db: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
h := Handler{
|
||||
Service: db,
|
||||
TokenVerifier: &jwt.TokenVerifier{},
|
||||
Service: database,
|
||||
TokenVerifier: &jwt.TokenVerifier{
|
||||
Secret: "this_is_my_demo_secret_for_unit_tests",
|
||||
},
|
||||
CredentialsVerifier: &bcrypt.Verifier{},
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
c, engine := gin.CreateTestContext(rr)
|
||||
recorder := httptest.NewRecorder()
|
||||
context, engine := gin.CreateTestContext(recorder)
|
||||
h.LoadRoutes(engine)
|
||||
|
||||
t.Run("RegisterUser", func(t *testing.T) {
|
||||
c.Request, err = http.NewRequest(http.MethodPost, "/api/v1/user/register", strings.NewReader(`{"password":"pass","email":"info@example.com","name":"Test"}`))
|
||||
t.Parallel()
|
||||
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(c)
|
||||
h.registerPost(context)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", recorder.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var response LoginResponse
|
||||
err = json.NewDecoder(rr.Body).Decode(&response)
|
||||
err = json.NewDecoder(recorder.Body).Decode(&response)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
t.Error("Error registering")
|
||||
@ -61,13 +67,14 @@ func TestListTimezonesHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("GetTransactions", func(t *testing.T) {
|
||||
c.Request, err = http.NewRequest(http.MethodGet, "/account/accountid/transactions", nil)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK)
|
||||
t.Parallel()
|
||||
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 TransactionsResponse
|
||||
err = json.NewDecoder(rr.Body).Decode(&response)
|
||||
err = json.NewDecoder(recorder.Body).Decode(&response)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
t.Error("Error retreiving list of transactions.")
|
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -1,7 +1,6 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
@ -13,7 +12,7 @@ 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"))
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"budgetid missing from URL"})
|
||||
return
|
||||
}
|
||||
|
||||
@ -35,7 +34,7 @@ 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"))
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"budgetid missing from URL"})
|
||||
return
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@ -14,18 +12,17 @@ type newBudgetInformation struct {
|
||||
|
||||
func (h *Handler) newBudget(c *gin.Context) {
|
||||
var newBudget newBudgetInformation
|
||||
err := c.BindJSON(&newBudget)
|
||||
if err != nil {
|
||||
if err := c.BindJSON(&newBudget); err != nil {
|
||||
c.AbortWithError(http.StatusNotAcceptable, err)
|
||||
return
|
||||
}
|
||||
|
||||
if newBudget.Name == "" {
|
||||
c.AbortWithError(http.StatusNotAcceptable, fmt.Errorf("Budget name is needed"))
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"budget name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.MustGet("token").(budgeteer.Token).GetID()
|
||||
userID := MustGetToken(c).GetID()
|
||||
budget, err := h.Service.NewBudget(c.Request.Context(), newBudget.Name, userID)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -55,7 +55,7 @@ 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"))
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"budgetid missing from URL"})
|
||||
return
|
||||
}
|
||||
|
||||
@ -80,16 +80,25 @@ func (h *Handler) budgetingForMonth(c *gin.Context) {
|
||||
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))
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorResponse{fmt.Sprintf("error loading balances: %s", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// skip everything in the future
|
||||
categoriesWithBalance, moneyUsed, err := h.calculateBalances(c, budget, firstOfNextMonth, firstOfMonth, categories, cumultativeBalances)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
categoriesWithBalance, moneyUsed := h.calculateBalances(
|
||||
budget, firstOfNextMonth, firstOfMonth, categories, cumultativeBalances)
|
||||
|
||||
availableBalance := h.getAvailableBalance(categories, budget, moneyUsed, cumultativeBalances, firstOfNextMonth)
|
||||
|
||||
data := struct {
|
||||
Categories []CategoryWithBalance
|
||||
AvailableBalance postgres.Numeric
|
||||
}{categoriesWithBalance, availableBalance}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func (*Handler) getAvailableBalance(categories []postgres.GetCategoriesRow, budget postgres.Budget,
|
||||
moneyUsed postgres.Numeric, cumultativeBalances []postgres.GetCumultativeBalancesRow,
|
||||
firstOfNextMonth time.Time) postgres.Numeric {
|
||||
availableBalance := postgres.NewZeroNumeric()
|
||||
for _, cat := range categories {
|
||||
if cat.ID != budget.IncomeCategoryID {
|
||||
@ -109,20 +118,14 @@ func (h *Handler) budgetingForMonth(c *gin.Context) {
|
||||
availableBalance = availableBalance.Add(bal.Transactions)
|
||||
}
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Categories []CategoryWithBalance
|
||||
AvailableBalance postgres.Numeric
|
||||
}{categoriesWithBalance, availableBalance}
|
||||
c.JSON(http.StatusOK, data)
|
||||
|
||||
return availableBalance
|
||||
}
|
||||
|
||||
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"))
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"budgetid missing from URL"})
|
||||
return
|
||||
}
|
||||
|
||||
@ -146,7 +149,9 @@ func (h *Handler) budgeting(c *gin.Context) {
|
||||
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) {
|
||||
func (h *Handler) calculateBalances(budget postgres.Budget,
|
||||
firstOfNextMonth time.Time, firstOfMonth time.Time, categories []postgres.GetCategoriesRow,
|
||||
cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, postgres.Numeric) {
|
||||
categoriesWithBalance := []CategoryWithBalance{}
|
||||
hiddenCategory := CategoryWithBalance{
|
||||
GetCategoriesRow: &postgres.GetCategoriesRow{
|
||||
@ -162,39 +167,9 @@ func (h *Handler) calculateBalances(c *gin.Context, budget postgres.Budget, firs
|
||||
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
|
||||
categoryWithBalance := h.CalculateCategoryBalances(cat, cumultativeBalances,
|
||||
firstOfNextMonth, &moneyUsed, firstOfMonth, hiddenCategory, budget)
|
||||
if cat.Group == "Hidden Categories" {
|
||||
hiddenCategory.Available = hiddenCategory.Available.Add(categoryWithBalance.Available)
|
||||
hiddenCategory.AvailableLastMonth = hiddenCategory.AvailableLastMonth.Add(categoryWithBalance.AvailableLastMonth)
|
||||
@ -212,5 +187,45 @@ func (h *Handler) calculateBalances(c *gin.Context, budget postgres.Budget, firs
|
||||
|
||||
categoriesWithBalance = append(categoriesWithBalance, hiddenCategory)
|
||||
|
||||
return categoriesWithBalance, moneyUsed, nil
|
||||
return categoriesWithBalance, moneyUsed
|
||||
}
|
||||
|
||||
func (*Handler) CalculateCategoryBalances(cat *postgres.GetCategoriesRow,
|
||||
cumultativeBalances []postgres.GetCumultativeBalancesRow, firstOfNextMonth time.Time,
|
||||
moneyUsed *postgres.Numeric, firstOfMonth time.Time, hiddenCategory CategoryWithBalance,
|
||||
budget postgres.Budget) CategoryWithBalance {
|
||||
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
|
||||
}
|
||||
|
||||
// skip everything in the future
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return categoryWithBalance
|
||||
}
|
@ -1,15 +1,14 @@
|
||||
package http
|
||||
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()
|
||||
userID := MustGetToken(c).GetID()
|
||||
budgets, err := h.Service.GetBudgetsForUser(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
return
|
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -11,12 +11,10 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// Handler handles incoming requests
|
||||
// Handler handles incoming requests.
|
||||
type Handler struct {
|
||||
Service *postgres.Database
|
||||
TokenVerifier budgeteer.TokenVerifier
|
||||
@ -24,25 +22,22 @@ type Handler struct {
|
||||
StaticFS http.FileSystem
|
||||
}
|
||||
|
||||
const (
|
||||
expiration = 72
|
||||
)
|
||||
|
||||
// Serve starts the http server
|
||||
// Serve starts the http server.
|
||||
func (h *Handler) Serve() {
|
||||
router := gin.Default()
|
||||
h.LoadRoutes(router)
|
||||
router.Run(":1323")
|
||||
|
||||
if err := router.Run(":1323"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadRoutes initializes all the routes
|
||||
func (h *Handler) LoadRoutes(router *gin.Engine) {
|
||||
static, err := fs.Sub(web.Static, "dist")
|
||||
if err != nil {
|
||||
panic("couldn't open static files")
|
||||
}
|
||||
h.StaticFS = http.FS(static)
|
||||
type ErrorResponse struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// LoadRoutes initializes all the routes.
|
||||
func (h *Handler) LoadRoutes(router *gin.Engine) {
|
||||
router.Use(enableCachingForStaticFiles())
|
||||
router.NoRoute(h.ServeStatic)
|
||||
|
||||
@ -81,6 +76,7 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
|
||||
transaction.POST("/new", h.newTransaction)
|
||||
transaction.POST("/:transactionid", h.newTransaction)
|
||||
}
|
||||
|
||||
func (h *Handler) ServeStatic(c *gin.Context) {
|
||||
h.ServeStaticFile(c, c.Request.URL.Path)
|
||||
}
|
||||
@ -108,7 +104,11 @@ func (h *Handler) ServeStaticFile(c *gin.Context, fullPath string) {
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), file.(io.ReadSeeker))
|
||||
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 {
|
@ -1,29 +1,36 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JSONDate time.Time
|
||||
|
||||
// Implement Marshaler and Unmarshaler interface
|
||||
// UnmarshalJSON parses the JSONDate from a JSON input.
|
||||
func (j *JSONDate) UnmarshalJSON(b []byte) error {
|
||||
s := strings.Trim(string(b), "\"")
|
||||
t, err := time.Parse("2006-01-02", s)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("parse date: %w", err)
|
||||
}
|
||||
*j = JSONDate(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON converts the JSONDate to a JSON in ISO format.
|
||||
func (j JSONDate) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Time(j))
|
||||
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
|
||||
// Format formats the time using the regular time.Time mechanics..
|
||||
func (j JSONDate) Format(s string) string {
|
||||
t := time.Time(j)
|
||||
return t.Format(s)
|
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -8,18 +8,34 @@ import (
|
||||
"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")
|
||||
const (
|
||||
HeaderName = "Authorization"
|
||||
Bearer = "Bearer "
|
||||
ParamName = "token"
|
||||
)
|
||||
|
||||
func MustGetToken(c *gin.Context) budgeteer.Token { //nolint:ireturn
|
||||
token := c.MustGet(ParamName)
|
||||
if token, ok := token.(budgeteer.Token); !ok {
|
||||
return token
|
||||
}
|
||||
|
||||
panic("Token is not a valid Token")
|
||||
}
|
||||
|
||||
func (h *Handler) verifyLogin(c *gin.Context) (budgeteer.Token, *ErrorResponse) { //nolint:ireturn
|
||||
tokenString := c.GetHeader(HeaderName)
|
||||
if len(tokenString) <= len(Bearer) {
|
||||
return nil, &ErrorResponse{"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 nil, &ErrorResponse{fmt.Sprintf("verify token '%s': %s", tokenString, err)}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
@ -28,12 +44,12 @@ func (h *Handler) verifyLogin(c *gin.Context) (budgeteer.Token, error) {
|
||||
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)
|
||||
// c.Header("WWW-Authenticate", "Bearer")
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("token", token)
|
||||
c.Set(ParamName, token)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
@ -45,7 +61,7 @@ func (h *Handler) verifyLoginWithRedirect(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("token", token)
|
||||
c.Set(ParamName, token)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
@ -72,19 +88,19 @@ func (h *Handler) loginPost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
t, err := h.TokenVerifier.CreateToken(&user)
|
||||
token, err := h.TokenVerifier.CreateToken(&user)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusUnauthorized, err)
|
||||
}
|
||||
|
||||
go h.Service.UpdateLastLogin(context.Background(), user.ID)
|
||||
go h.UpdateLastLogin(user.ID)
|
||||
|
||||
budgets, err := h.Service.GetBudgetsForUser(c.Request.Context(), user.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, LoginResponse{t, user, budgets})
|
||||
c.JSON(http.StatusOK, LoginResponse{token, user, budgets})
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
@ -101,16 +117,20 @@ type registerInformation struct {
|
||||
|
||||
func (h *Handler) registerPost(c *gin.Context) {
|
||||
var register registerInformation
|
||||
c.BindJSON(®ister)
|
||||
|
||||
if register.Email == "" || register.Password == "" || register.Name == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("e-mail, password and name are required"))
|
||||
err := c.BindJSON(®ister)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"error parsing body"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.Service.GetUserByUsername(c.Request.Context(), register.Email)
|
||||
if register.Email == "" || register.Password == "" || register.Name == "" {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"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"))
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"email is already taken"})
|
||||
return
|
||||
}
|
||||
|
||||
@ -130,17 +150,24 @@ func (h *Handler) registerPost(c *gin.Context) {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
t, err := h.TokenVerifier.CreateToken(&user)
|
||||
token, err := h.TokenVerifier.CreateToken(&user)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusUnauthorized, err)
|
||||
}
|
||||
|
||||
go h.Service.UpdateLastLogin(context.Background(), user.ID)
|
||||
go h.UpdateLastLogin(user.ID)
|
||||
|
||||
budgets, err := h.Service.GetBudgetsForUser(c.Request.Context(), user.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, LoginResponse{t, user, budgets})
|
||||
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)
|
||||
}
|
||||
}
|
60
server/transaction.go
Normal file
60
server/transaction.go
Normal file
@ -0,0 +1,60 @@
|
||||
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:"budgetId"`
|
||||
AccountID uuid.UUID `json:"accountId"`
|
||||
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))
|
||||
return
|
||||
}
|
||||
|
||||
newTransaction := postgres.CreateTransactionParams{
|
||||
Memo: payload.Memo,
|
||||
Date: time.Time(payload.Date),
|
||||
Amount: amount,
|
||||
AccountID: payload.AccountID,
|
||||
PayeeID: payload.Payee.ID,
|
||||
CategoryID: payload.Category.ID,
|
||||
Status: postgres.TransactionStatus(payload.State),
|
||||
}
|
||||
_, err = h.Service.CreateTransaction(c.Request.Context(), newTransaction)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package http
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
@ -12,7 +11,7 @@ import (
|
||||
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"))
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"no budget_id specified"})
|
||||
return
|
||||
}
|
||||
|
||||
@ -40,7 +39,7 @@ func (h *Handler) importYNAB(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = ynab.ImportTransactions(transactions)
|
||||
err = ynab.ImportTransactions(c.Request.Context(), transactions)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
@ -58,7 +57,7 @@ func (h *Handler) importYNAB(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = ynab.ImportAssignments(assignments)
|
||||
err = ynab.ImportAssignments(c.Request.Context(), assignments)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
4
token.go
4
token.go
@ -5,7 +5,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Token contains data that authenticates a user
|
||||
// Token contains data that authenticates a user.
|
||||
type Token interface {
|
||||
GetUsername() string
|
||||
GetName() string
|
||||
@ -13,7 +13,7 @@ type Token interface {
|
||||
GetID() uuid.UUID
|
||||
}
|
||||
|
||||
// TokenVerifier verifies a Token
|
||||
// TokenVerifier verifies a Token.
|
||||
type TokenVerifier interface {
|
||||
VerifyToken(string) (Token, error)
|
||||
CreateToken(*postgres.User) (string, error)
|
||||
|
@ -12,7 +12,7 @@ onMounted(() => {
|
||||
|
||||
function formSubmit(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
useSessionStore().login(login)
|
||||
useSessionStore().login(login.value)
|
||||
.then(x => {
|
||||
error.value = "";
|
||||
useRouter().replace("/dashboard");
|
||||
|
Loading…
x
Reference in New Issue
Block a user