Compare commits
254 Commits
v0.0.1
...
2843d8a2f1
Author | SHA1 | Date | |
---|---|---|---|
2843d8a2f1 | |||
843dcd2536 | |||
a147830e12 | |||
b0776023b4 | |||
0b95cdc1d9 | |||
2ec9c923df | |||
beff7afcf7 | |||
951e827d20 | |||
2f3e4bc748 | |||
d71eb17092 | |||
53dd31fa35 | |||
1a4267186a | |||
5018e5b973 | |||
ed9e75d57a | |||
ed361324dd | |||
6bac09a38e | |||
ab43387f06 | |||
c112d95a41 | |||
6fdc0e3b1d | |||
f08784ffa7 | |||
8188184ac9 | |||
81b3bf334a | |||
d0ad0dcb3a | |||
1ab1fa74e0 | |||
33c54c9f4c | |||
1ed9344586 | |||
a8bd03a805 | |||
9e01be699a | |||
84ddb36d62 | |||
8b6a8c3697 | |||
208ffce968 | |||
bfba5f4028 | |||
1f2d81f173 | |||
c3a93377d9 | |||
40a299141d | |||
935499e3a8 | |||
915964fa4e | |||
e9adc763b2 | |||
d5ebf5a5cf | |||
466775817f | |||
e2413290b4 | |||
18cd29cca2 | |||
caf0126b86 | |||
6da1b26a2f | |||
13993b6b5a | |||
625e0635fd | |||
1826274ccc | |||
defbbd1884 | |||
8116238d48 | |||
e0eeaadc60 | |||
4cd81592e4 | |||
5d9693838f | |||
3bec0857d5 | |||
5e18d51b5d | |||
11179a1593 | |||
7cb7527704 | |||
c3a022b595 | |||
a0ebdd01aa | |||
edd1319222 | |||
a19d3d6932 | |||
f4ddf12214 | |||
04fd687324 | |||
cbda69e827 | |||
e3f3dc6748 | |||
915379f5cb | |||
284685fb52 | |||
5f4c5d9d51 | |||
8c9c78a789 | |||
64822912d9 | |||
1d4bc158a8 | |||
fbd283cd1c | |||
0ee3f269b5 | |||
6bcf94661e | |||
0b0b20c5ec | |||
6f4bff929e | |||
4a0759af8f | |||
646560267a | |||
53c51ceb8d | |||
8f374f1d62 | |||
494c1431fe | |||
82045ceed7 | |||
16b59afc29 | |||
29cee46a14 | |||
2caaa1a048 | |||
e3cf69ab08 | |||
1437fc7b8d | |||
e2d22a1080 | |||
495bc2b7c3 | |||
674028d394 | |||
f103cfe3e3 | |||
21db0fb3aa | |||
779733e0ab | |||
2ec3873e83 | |||
91a0c43d6d | |||
69bac9bc3f | |||
95f6e95fdd | |||
5321c7d85f | |||
ba0926900a | |||
61fa6ed776 | |||
6628a4849f | |||
e30fab6a06 | |||
cb1297b8bf | |||
223064698e | |||
c56335adad | |||
871b11bbcc | |||
f2e8721aa8 | |||
4646356b2d | |||
6f8a94ff5d | |||
276fdb4ade | |||
a4659c7133 | |||
696469f6c2 | |||
db7f0bfb7b | |||
0855942d0d | |||
dcdcd53833 | |||
11d20eeb66 | |||
4011f3cace | |||
a3df95a700 | |||
36bccce021 | |||
e7c9a7f52f | |||
67139ceff8 | |||
61c55dda3a | |||
e9173a3d37 | |||
57542b5202 | |||
1cd2eedcb8 | |||
7b6914e5f2 | |||
63c1b4fbab | |||
77ae9d2dfd | |||
67f7022b90 | |||
f760f9b855 | |||
e465b961a5 | |||
cdc767a497 | |||
adce350e0c | |||
4d1b883974 | |||
37d19733df | |||
6df72dc40d | |||
f0a1d1d475 | |||
01e634333a | |||
8e8c653fc9 | |||
85ef7557c1 | |||
5e8a98872f | |||
8aa147f2a5 | |||
45d01b4e84 | |||
c4fe36a2a4 | |||
bda3d32cfc | |||
f40a05f92d | |||
19fef9cc05 | |||
14df8ff84b | |||
5de7d32c30 | |||
cf1bc70103 | |||
f019c47d21 | |||
bc315961af | |||
736343a3dd | |||
30b306e485 | |||
a891047b7a | |||
4b08de00b8 | |||
f33d4b6cae | |||
4658b40bd9 | |||
9bc0f2054e | |||
5128ac4c3b | |||
06f7a57565 | |||
9b04ea8dc6 | |||
2eb1457019 | |||
5518d57bbd | |||
caa10ee560 | |||
39bd56a30f | |||
2e8f21ea8c | |||
b76f720156 | |||
ddbda095b5 | |||
ddf66dd8b0 | |||
c598694eb4 | |||
019ffa34cd | |||
725555bdc9 | |||
a504e4d382 | |||
2619566a62 | |||
9e7701cf06 | |||
4bdc292570 | |||
9a399c13a3 | |||
917251a16e | |||
073b17b4c0 | |||
e8574ebd0a | |||
1ee88466f2 | |||
e525fdc928 | |||
5b9b2e02a0 | |||
30d9da1c81 | |||
5754b97e4d | |||
0037cf045c | |||
649b272caf | |||
7c8698da86 | |||
f36fe1b1c0 | |||
72fbec1063 | |||
57a69c448d | |||
07d5816251 | |||
672376a55c | |||
04af4bce7e | |||
ca1c2ed778 | |||
135b1b8e8d | |||
9a206250e8 | |||
aba366f211 | |||
2b2f508998 | |||
7c197ff49d | |||
83c36f00bf | |||
6a8818dab6 | |||
97ca85a0db | |||
518a0c2df9 | |||
4b7a70d03d | |||
a71afaf6b9 | |||
b9d428d386 | |||
cf03726643 | |||
88eb29f855 | |||
3406ce58cf | |||
9db0ea1870 | |||
6b730d5b33 | |||
2c55fbc431 | |||
b5114beacf | |||
e955638510 | |||
7b235f83ad | |||
cb6558a8ce | |||
f66e544d43 | |||
dafc477fe8 | |||
2de8e46bf3 | |||
528f329657 | |||
65322bc182 | |||
c3664ef3e0 | |||
0f1bd4cac3 | |||
308ff98830 | |||
02d6f3bd79 | |||
1592e4e8cf | |||
8231b3d176 | |||
227028f99d | |||
0cac7a69aa | |||
099ae5fe8a | |||
c0d6ae1157 | |||
f759600db1 | |||
28b26c088e | |||
f4e227957b | |||
39141ab2e6 | |||
f74b7ab078 | |||
ed0b939a75 | |||
1fa6c43df1 | |||
147219e66d | |||
7a5bc35cc3 | |||
e4755bcad8 | |||
36b066c687 | |||
304b954b51 | |||
bb6fd11a92 | |||
637efda810 | |||
825bf7be04 | |||
0adbb9087e | |||
c8f221b310 | |||
3f8b88ecd4 | |||
ba7cea9965 | |||
db18731e43 | |||
a122e5db0c | |||
b2ed65788e |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
build/
|
||||||
|
.git/
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
Earthfile
|
||||||
|
config.example.json
|
||||||
|
.gitignore
|
||||||
|
.vscode/
|
||||||
|
budgeteer
|
||||||
|
budgeteer.exe
|
27
.drone.yml
Normal file
27
.drone.yml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: budgeteer
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Taskfile.dev
|
||||||
|
image: hub.javil.eu/budgeteer:dev
|
||||||
|
commands:
|
||||||
|
- task build
|
||||||
|
|
||||||
|
- name: docker
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
registry: hub.javil.eu
|
||||||
|
username:
|
||||||
|
from_secret: docker_user
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
repo: hub.javil.eu/budgeteer
|
||||||
|
context: build
|
||||||
|
dockerfile: build/Dockerfile
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
|
||||||
|
image_pull_secrets:
|
||||||
|
- hub.javil.eu
|
10
.earthignore
Normal file
10
.earthignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
build/
|
||||||
|
.git/
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
Earthfile
|
||||||
|
config.example.json
|
||||||
|
.gitignore
|
||||||
|
.vscode/
|
||||||
|
budgeteer
|
||||||
|
budgeteer.exe
|
99
.gitignore
vendored
99
.gitignore
vendored
@ -1,27 +1,88 @@
|
|||||||
# ---> Go
|
# From https://stackoverflow.com/questions/5711120/gitignore-binary-files-that-have-no-extension
|
||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
# Ignore all
|
||||||
*.o
|
*
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
# Unignore all with extensions
|
||||||
_obj
|
!*.*
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
# Unignore all dirs
|
||||||
*.[568vq]
|
!*/
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,sublimetext,go
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,sublimetext,go
|
||||||
|
|
||||||
|
### Go ###
|
||||||
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.exe~
|
||||||
*.prof
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
### Go Patch ###
|
||||||
|
/vendor/
|
||||||
|
/Godeps/
|
||||||
|
|
||||||
|
### SublimeText ###
|
||||||
|
# Cache files for Sublime Text
|
||||||
|
*.tmlanguage.cache
|
||||||
|
*.tmPreferences.cache
|
||||||
|
*.stTheme.cache
|
||||||
|
|
||||||
|
# Workspace files are user-specific
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Project files should be checked into the repository, unless a significant
|
||||||
|
# proportion of contributors will probably not be using Sublime Text
|
||||||
|
# *.sublime-project
|
||||||
|
|
||||||
|
# SFTP configuration file
|
||||||
|
sftp-config.json
|
||||||
|
sftp-config-alt*.json
|
||||||
|
|
||||||
|
# Package control specific files
|
||||||
|
Package Control.last-run
|
||||||
|
Package Control.ca-list
|
||||||
|
Package Control.ca-bundle
|
||||||
|
Package Control.system-ca-bundle
|
||||||
|
Package Control.cache/
|
||||||
|
Package Control.ca-certs/
|
||||||
|
Package Control.merged-ca-bundle
|
||||||
|
Package Control.user-ca-bundle
|
||||||
|
oscrypto-ca-bundle.crt
|
||||||
|
bh_unicode_properties.cache
|
||||||
|
|
||||||
|
# Sublime-github package stores a github token in this file
|
||||||
|
# https://packagecontrol.io/packages/sublime-github
|
||||||
|
GitHub.sublime-settings
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
# Support for Project snippet scope
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,sublimetext,go
|
24
.vscode/tasks.json
vendored
Normal file
24
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "task watch +run",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "task -w run",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "earthly +run",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "earthly +run",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
Dockerfile.dev
Normal file
3
Dockerfile.dev
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
FROM golang:1.17
|
||||||
|
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
|
||||||
|
RUN go install github.com/go-task/task/v3/cmd/task@latest
|
21
Earthfile
Normal file
21
Earthfile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
FROM golang:1.17
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
build:
|
||||||
|
COPY go.mod go.sum .
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build go build -o build/budgeteer ./cmd/budgeteer
|
||||||
|
SAVE ARTIFACT build/budgeteer /budgeteer AS LOCAL build/budgeteer
|
||||||
|
|
||||||
|
docker:
|
||||||
|
WORKDIR /app
|
||||||
|
COPY +build/budgeteer .
|
||||||
|
ENTRYPOINT ["/app/budgeteer"]
|
||||||
|
SAVE IMAGE budgeteer:latest
|
||||||
|
|
||||||
|
run:
|
||||||
|
LOCALLY
|
||||||
|
WITH DOCKER --load=+docker
|
||||||
|
RUN docker-compose up -d
|
||||||
|
END
|
61
Taskfile.yml
Normal file
61
Taskfile.yml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
cmds:
|
||||||
|
- task: build
|
||||||
|
|
||||||
|
sqlc:
|
||||||
|
desc: sqlc code generation
|
||||||
|
sources:
|
||||||
|
- ./sqlc.yaml
|
||||||
|
- ./postgres/schema/*
|
||||||
|
- ./postgres/queries/*
|
||||||
|
generates:
|
||||||
|
- ./postgres/*.sql.go
|
||||||
|
cmds:
|
||||||
|
- sqlc generate
|
||||||
|
|
||||||
|
gomod:
|
||||||
|
desc: Go modules
|
||||||
|
sources:
|
||||||
|
- ./go.mod
|
||||||
|
- ./go.sum
|
||||||
|
method: checksum
|
||||||
|
cmds:
|
||||||
|
- go mod download
|
||||||
|
|
||||||
|
build:
|
||||||
|
desc: Build budgeteer
|
||||||
|
deps: [gomod, sqlc]
|
||||||
|
sources:
|
||||||
|
- ./go.mod
|
||||||
|
- ./go.sum
|
||||||
|
- ./cmd/budgeteer/*.go
|
||||||
|
- ./*.go
|
||||||
|
- ./config/*.go
|
||||||
|
- ./http/*.go
|
||||||
|
- ./jwt/*.go
|
||||||
|
- ./postgres/*.go
|
||||||
|
- ./web/**/*
|
||||||
|
- ./postgres/schema/*
|
||||||
|
generates:
|
||||||
|
- build/budgeteer{{exeExt}}
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
cmds:
|
||||||
|
- go build -o ./build/budgeteer{{exeExt}} ./cmd/budgeteer
|
||||||
|
|
||||||
|
docker:
|
||||||
|
desc: Build budgeeter:latest
|
||||||
|
deps: [build]
|
||||||
|
sources:
|
||||||
|
- ./build/budgeteer
|
||||||
|
cmds:
|
||||||
|
- docker build -t budgeteer:latest -t hub.javil.eu/budgeteer:latest ./build
|
||||||
|
|
||||||
|
run:
|
||||||
|
desc: Start docker-compose
|
||||||
|
deps: [docker]
|
||||||
|
cmds:
|
||||||
|
- docker-compose up -d
|
23
bcrypt/verifier.go
Normal file
23
bcrypt/verifier.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"folders":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"path": "."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
3
build/Dockerfile
Normal file
3
build/Dockerfile
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
FROM scratch
|
||||||
|
COPY ./budgeteer /app/budgeteer
|
||||||
|
ENTRYPOINT ["/app/budgeteer"]
|
34
cmd/budgeteer/main.go
Normal file
34
cmd/budgeteer/main.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bv := &bcrypt.Verifier{}
|
||||||
|
|
||||||
|
q, err := postgres.Connect(cfg.DatabaseHost, cfg.DatabaseUser, cfg.DatabasePassword, cfg.DatabaseName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed connecting to DB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tv := &jwt.TokenVerifier{}
|
||||||
|
|
||||||
|
h := &http.Handler{
|
||||||
|
Service: q,
|
||||||
|
TokenVerifier: tv,
|
||||||
|
CredentialsVerifier: bv,
|
||||||
|
}
|
||||||
|
h.Serve()
|
||||||
|
}
|
6
config.example.json
Normal file
6
config.example.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"DatabaseHost": "localhost",
|
||||||
|
"DatabaseUser": "user",
|
||||||
|
"DatabasePassword": "thisismypassword",
|
||||||
|
"DatabaseName": "budgeteer"
|
||||||
|
}
|
29
config/config.go
Normal file
29
config/config.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains all needed configurations
|
||||||
|
type Config struct {
|
||||||
|
DatabaseUser string
|
||||||
|
DatabaseHost string
|
||||||
|
DatabasePassword string
|
||||||
|
DatabaseName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig from path
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
configuration := Config{
|
||||||
|
DatabaseUser: os.Getenv("BUDGETEER_DB_USER"),
|
||||||
|
DatabaseHost: os.Getenv("BUDGETEER_DB_HOST"),
|
||||||
|
DatabasePassword: os.Getenv("BUDGETEER_DB_PASS"),
|
||||||
|
DatabaseName: os.Getenv("BUDGETEER_DB_NAME"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.DatabaseName == "" {
|
||||||
|
configuration.DatabaseName = "budgeteer"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &configuration, nil
|
||||||
|
}
|
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: budgeteer:latest
|
||||||
|
container_name: budgeteer
|
||||||
|
ports:
|
||||||
|
- 1323:1323
|
||||||
|
environment:
|
||||||
|
BUDGETEER_DB_NAME: budgeteer
|
||||||
|
BUDGETEER_DB_USER: budgeteer
|
||||||
|
BUDGETEER_DB_PASS: budgeteer
|
||||||
|
BUDGETEER_DB_HOST: db:5432
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:14
|
||||||
|
volumes:
|
||||||
|
- db:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: budgeteer
|
||||||
|
POSTGRES_PASSWORD: budgeteer
|
||||||
|
POSTGRES_DBE: budgeteer
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
ports:
|
||||||
|
- 1424:8080
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db:
|
39
go.mod
Normal file
39
go.mod
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
module git.javil.eu/jacob1123/budgeteer
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
|
github.com/gin-gonic/gin v1.7.4
|
||||||
|
github.com/google/uuid v1.3.0
|
||||||
|
github.com/jackc/pgx/v4 v4.13.0
|
||||||
|
github.com/pressly/goose/v3 v3.3.1
|
||||||
|
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.13.0 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.0 // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgconn v1.10.0 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
|
github.com/jackc/pgtype v1.8.1 // direct
|
||||||
|
github.com/json-iterator/go v1.1.9 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/shopspring/decimal v1.3.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
|
||||||
|
golang.org/x/text v0.3.6 // indirect
|
||||||
|
google.golang.org/protobuf v1.26.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||||
|
)
|
439
go.sum
Normal file
439
go.sum
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM=
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/ClickHouse/clickhouse-go v1.5.1/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
|
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||||
|
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
|
||||||
|
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
||||||
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
|
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
||||||
|
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
|
github.com/containerd/continuity v0.2.1/go.mod h1:wCYX+dRqZdImhGucXOqTQn05AhX6EUDaGEMUzTFFpLg=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
|
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/docker/cli v20.10.8+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
|
||||||
|
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||||
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||||
|
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||||
|
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||||
|
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||||
|
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||||
|
github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU=
|
||||||
|
github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||||
|
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||||
|
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||||
|
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||||
|
github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs=
|
||||||
|
github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||||
|
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||||
|
github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570=
|
||||||
|
github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
|
||||||
|
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
|
||||||
|
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||||
|
github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
|
||||||
|
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||||
|
github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
|
||||||
|
github.com/ory/dockertest/v3 v3.8.0/go.mod h1:9zPATATlWQru+ynXP+DytBQrsXV7Tmlx7K86H6fQaDo=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pressly/goose/v3 v3.3.1 h1:Jjkzxj0lCmy1aI70CKJAX6ObBk8mTwsm8/zYCCR7Inw=
|
||||||
|
github.com/pressly/goose/v3 v3.3.1/go.mod h1:6sKWO0jRWFnDAg98QI4cTzTPuUR9EMF3l27I2UmD9sc=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||||
|
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
|
||||||
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
|
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||||
|
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||||
|
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
|
||||||
|
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
|
||||||
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||||
|
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
54
http/account.go
Normal file
54
http/account.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountData struct {
|
||||||
|
AlwaysNeededData
|
||||||
|
Account *postgres.Account
|
||||||
|
Categories []postgres.GetCategoriesRow
|
||||||
|
Transactions []postgres.GetTransactionsForAccountRow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) account(c *gin.Context) {
|
||||||
|
data := c.MustGet("data").(AlwaysNeededData)
|
||||||
|
|
||||||
|
accountID := c.Param("accountid")
|
||||||
|
accountUUID, err := uuid.Parse(accountID)
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := h.Service.GetAccount(c.Request.Context(), accountUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
categories, err := h.Service.GetCategories(c.Request.Context(), data.Budget.ID)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
d := AccountData{
|
||||||
|
data,
|
||||||
|
&account,
|
||||||
|
categories,
|
||||||
|
transactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "account.html", d)
|
||||||
|
}
|
19
http/accounts.go
Normal file
19
http/accounts.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountsData struct {
|
||||||
|
AlwaysNeededData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) accounts(c *gin.Context) {
|
||||||
|
d := AccountsData{
|
||||||
|
c.MustGet("data").(AlwaysNeededData),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "accounts.html", d)
|
||||||
|
}
|
117
http/admin.go
Normal file
117
http/admin.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminData struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) admin(c *gin.Context) {
|
||||||
|
d := AdminData{}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "admin.html", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) clearDatabase(c *gin.Context) {
|
||||||
|
d := AdminData{}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "admin.html", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsData struct {
|
||||||
|
AlwaysNeededData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) settings(c *gin.Context) {
|
||||||
|
d := SettingsData{
|
||||||
|
c.MustGet("data").(AlwaysNeededData),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "settings.html", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) clearBudget(c *gin.Context) {
|
||||||
|
budgetID := c.Param("budgetid")
|
||||||
|
budgetUUID, err := uuid.Parse(budgetID)
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
}
|
57
http/always-needed-data.go
Normal file
57
http/always-needed-data.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlwaysNeededData struct {
|
||||||
|
Budget postgres.Budget
|
||||||
|
Accounts []postgres.GetAccountsWithBalanceRow
|
||||||
|
OnBudgetAccounts []postgres.GetAccountsWithBalanceRow
|
||||||
|
OffBudgetAccounts []postgres.GetAccountsWithBalanceRow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) getImportantData(c *gin.Context) {
|
||||||
|
budgetID := c.Param("budgetid")
|
||||||
|
budgetUUID, err := uuid.Parse(budgetID)
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||||
|
c.Abort()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var onBudgetAccounts, offBudgetAccounts []postgres.GetAccountsWithBalanceRow
|
||||||
|
for _, account := range accounts {
|
||||||
|
if account.OnBudget {
|
||||||
|
onBudgetAccounts = append(onBudgetAccounts, account)
|
||||||
|
} else {
|
||||||
|
offBudgetAccounts = append(offBudgetAccounts, account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base := AlwaysNeededData{
|
||||||
|
Accounts: accounts,
|
||||||
|
OnBudgetAccounts: onBudgetAccounts,
|
||||||
|
OffBudgetAccounts: offBudgetAccounts,
|
||||||
|
Budget: budget,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("data", base)
|
||||||
|
c.Next()
|
||||||
|
}
|
64
http/budget.go
Normal file
64
http/budget.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer"
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AllAccountsData struct {
|
||||||
|
AlwaysNeededData
|
||||||
|
Account *postgres.Account
|
||||||
|
Categories []postgres.GetCategoriesRow
|
||||||
|
Transactions []postgres.GetTransactionsForBudgetRow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) allAccounts(c *gin.Context) {
|
||||||
|
budgetID := c.Param("budgetid")
|
||||||
|
budgetUUID, err := uuid.Parse(budgetID)
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
categories, err := h.Service.GetCategories(c.Request.Context(), budgetUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions, err := h.Service.GetTransactionsForBudget(c.Request.Context(), budgetUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d := AllAccountsData{
|
||||||
|
c.MustGet("data").(AlwaysNeededData),
|
||||||
|
&postgres.Account{
|
||||||
|
Name: "All accounts",
|
||||||
|
},
|
||||||
|
categories,
|
||||||
|
transactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "account.html", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) newBudget(c *gin.Context) {
|
||||||
|
budgetName, succ := c.GetPostForm("name")
|
||||||
|
if !succ {
|
||||||
|
c.AbortWithStatus(http.StatusNotAcceptable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := c.MustGet("token").(budgeteer.Token).GetID()
|
||||||
|
_, err := h.Service.NewBudget(c.Request.Context(), budgetName, userID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
182
http/budgeting.go
Normal file
182
http/budgeting.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BudgetingData struct {
|
||||||
|
AlwaysNeededData
|
||||||
|
Categories []CategoryWithBalance
|
||||||
|
AvailableBalance float64
|
||||||
|
Date time.Time
|
||||||
|
Next time.Time
|
||||||
|
Previous time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
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 float64
|
||||||
|
AvailableLastMonth float64
|
||||||
|
Activity float64
|
||||||
|
Assigned float64
|
||||||
|
}
|
||||||
|
|
||||||
|
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) budgeting(c *gin.Context) {
|
||||||
|
alwaysNeededData := c.MustGet("data").(AlwaysNeededData)
|
||||||
|
budgetUUID := alwaysNeededData.Budget.ID
|
||||||
|
|
||||||
|
firstOfMonth, err := getDate(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budgetUUID.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
firstOfNextMonth := firstOfMonth.AddDate(0, 1, 0)
|
||||||
|
firstOfPreviousMonth := firstOfMonth.AddDate(0, -1, 0)
|
||||||
|
d := BudgetingData{
|
||||||
|
AlwaysNeededData: alwaysNeededData,
|
||||||
|
Date: firstOfMonth,
|
||||||
|
Next: firstOfNextMonth,
|
||||||
|
Previous: firstOfPreviousMonth,
|
||||||
|
}
|
||||||
|
|
||||||
|
categories, err := h.Service.GetCategories(c.Request.Context(), budgetUUID)
|
||||||
|
|
||||||
|
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, alwaysNeededData.Budget, firstOfNextMonth, firstOfMonth, categories, cumultativeBalances)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.Categories = categoriesWithBalance
|
||||||
|
|
||||||
|
data := c.MustGet("data").(AlwaysNeededData)
|
||||||
|
var availableBalance float64 = 0
|
||||||
|
for _, cat := range categories {
|
||||||
|
if cat.ID != data.Budget.IncomeCategoryID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
availableBalance = moneyUsed
|
||||||
|
|
||||||
|
for _, bal := range cumultativeBalances {
|
||||||
|
if bal.CategoryID != cat.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bal.Date.Before(firstOfNextMonth) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
availableBalance += bal.Transactions.GetFloat64()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.AvailableBalance = availableBalance
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "budgeting.html", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) calculateBalances(c *gin.Context, budget postgres.Budget, firstOfNextMonth time.Time, firstOfMonth time.Time, categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, float64, error) {
|
||||||
|
categoriesWithBalance := []CategoryWithBalance{}
|
||||||
|
hiddenCategory := CategoryWithBalance{
|
||||||
|
GetCategoriesRow: &postgres.GetCategoriesRow{
|
||||||
|
Name: "",
|
||||||
|
Group: "Hidden Categories",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var moneyUsed float64 = 0
|
||||||
|
for i := range categories {
|
||||||
|
cat := &categories[i]
|
||||||
|
categoryWithBalance := CategoryWithBalance{
|
||||||
|
GetCategoriesRow: cat,
|
||||||
|
}
|
||||||
|
for _, bal := range cumultativeBalances {
|
||||||
|
if bal.CategoryID != cat.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bal.Date.Before(firstOfNextMonth) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
moneyUsed -= bal.Assignments.GetFloat64()
|
||||||
|
categoryWithBalance.Available += bal.Assignments.GetFloat64()
|
||||||
|
categoryWithBalance.Available += bal.Transactions.GetFloat64()
|
||||||
|
if categoryWithBalance.Available < 0 && bal.Date.Before(firstOfMonth) {
|
||||||
|
moneyUsed += categoryWithBalance.Available
|
||||||
|
categoryWithBalance.Available = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if bal.Date.Before(firstOfMonth) {
|
||||||
|
categoryWithBalance.AvailableLastMonth = categoryWithBalance.Available
|
||||||
|
} else if bal.Date.Before(firstOfNextMonth) {
|
||||||
|
categoryWithBalance.Activity = bal.Transactions.GetFloat64()
|
||||||
|
categoryWithBalance.Assigned = bal.Assignments.GetFloat64()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not show hidden categories
|
||||||
|
if cat.Group == "Hidden Categories" {
|
||||||
|
hiddenCategory.Available += categoryWithBalance.Available
|
||||||
|
hiddenCategory.AvailableLastMonth += categoryWithBalance.AvailableLastMonth
|
||||||
|
hiddenCategory.Activity += categoryWithBalance.Activity
|
||||||
|
hiddenCategory.Assigned += categoryWithBalance.Assigned
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cat.ID == budget.IncomeCategoryID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
categoriesWithBalance = append(categoriesWithBalance, categoryWithBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
categoriesWithBalance = append(categoriesWithBalance, hiddenCategory)
|
||||||
|
|
||||||
|
return categoriesWithBalance, moneyUsed, nil
|
||||||
|
}
|
26
http/dashboard.go
Normal file
26
http/dashboard.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
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.HTML(http.StatusOK, "dashboard.html", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardData struct {
|
||||||
|
Budgets []postgres.Budget
|
||||||
|
}
|
101
http/http.go
Normal file
101
http/http.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer"
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/bcrypt"
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/web"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler handles incoming requests
|
||||||
|
type Handler struct {
|
||||||
|
Service *postgres.Database
|
||||||
|
TokenVerifier budgeteer.TokenVerifier
|
||||||
|
CredentialsVerifier *bcrypt.Verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
expiration = 72
|
||||||
|
authCookie = "authentication"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Serve starts the HTTP Server
|
||||||
|
func (h *Handler) Serve() {
|
||||||
|
router := gin.Default()
|
||||||
|
router.FuncMap["now"] = time.Now
|
||||||
|
|
||||||
|
templates, err := NewTemplates(router.FuncMap)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.HTMLRender = templates
|
||||||
|
|
||||||
|
static, err := fs.Sub(web.Static, "static")
|
||||||
|
if err != nil {
|
||||||
|
panic("couldn't open static files")
|
||||||
|
}
|
||||||
|
router.Use(enableCachingForStaticFiles())
|
||||||
|
router.StaticFS("/static", http.FS(static))
|
||||||
|
|
||||||
|
router.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) })
|
||||||
|
router.GET("/login", h.login)
|
||||||
|
router.GET("/register", h.register)
|
||||||
|
|
||||||
|
withLogin := router.Group("")
|
||||||
|
withLogin.Use(h.verifyLoginWithRedirect)
|
||||||
|
withLogin.GET("/dashboard", h.dashboard)
|
||||||
|
withLogin.GET("/admin", h.admin)
|
||||||
|
withLogin.GET("/admin/clear-database", h.clearDatabase)
|
||||||
|
|
||||||
|
withBudget := router.Group("")
|
||||||
|
withBudget.Use(h.verifyLoginWithRedirect)
|
||||||
|
withBudget.Use(h.getImportantData)
|
||||||
|
withBudget.GET("/budget/:budgetid", h.budgeting)
|
||||||
|
withBudget.GET("/budget/:budgetid/:year/:month", h.budgeting)
|
||||||
|
withBudget.GET("/budget/:budgetid/all-accounts", h.allAccounts)
|
||||||
|
withBudget.GET("/budget/:budgetid/accounts", h.accounts)
|
||||||
|
withBudget.GET("/budget/:budgetid/account/:accountid", h.account)
|
||||||
|
withBudget.GET("/budget/:budgetid/settings", h.settings)
|
||||||
|
withBudget.GET("/budget/:budgetid/settings/clear", h.clearBudget)
|
||||||
|
withBudget.GET("/budget/:budgetid/settings/clean-negative", h.cleanNegativeBudget)
|
||||||
|
withBudget.GET("/budget/:budgetid/transaction/:transactionid", h.transaction)
|
||||||
|
|
||||||
|
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.verifyLoginWithRedirect)
|
||||||
|
|
||||||
|
user := authenticated.Group("/user")
|
||||||
|
user.GET("/logout", logout)
|
||||||
|
|
||||||
|
budget := authenticated.Group("/budget")
|
||||||
|
budget.POST("/new", h.newBudget)
|
||||||
|
|
||||||
|
transaction := authenticated.Group("/transaction")
|
||||||
|
transaction.POST("/new", h.newTransaction)
|
||||||
|
transaction.POST("/:transactionid", h.newTransaction)
|
||||||
|
transaction.POST("/import/ynab", h.importYNAB)
|
||||||
|
|
||||||
|
router.Run(":1323")
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableCachingForStaticFiles() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if strings.HasPrefix(c.Request.RequestURI, "/static/") {
|
||||||
|
c.Header("Cache-Control", "max-age=86400")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
http/session.go
Normal file
122
http/session.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer"
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) verifyLogin(c *gin.Context) (budgeteer.Token, error) {
|
||||||
|
tokenString, err := c.Cookie(authCookie)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get cookie: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := h.TokenVerifier.VerifyToken(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
c.SetCookie(authCookie, "", -1, "", "", false, false)
|
||||||
|
return nil, fmt.Errorf("verify token '%s': %w", tokenString, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) login(c *gin.Context) {
|
||||||
|
if _, err := h.verifyLogin(c); err == nil {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/dashboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "login.html", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) register(c *gin.Context) {
|
||||||
|
if _, err := h.verifyLogin(c); err == nil {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/dashboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "register.html", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logout(c *gin.Context) {
|
||||||
|
clearLogin(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearLogin(c *gin.Context) {
|
||||||
|
c.SetCookie(authCookie, "", -1, "", "", false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) loginPost(c *gin.Context) {
|
||||||
|
username, _ := c.GetPostForm("username")
|
||||||
|
password, _ := c.GetPostForm("password")
|
||||||
|
|
||||||
|
user, err := h.Service.GetUserByUsername(c.Request.Context(), username)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = h.CredentialsVerifier.Verify(password, user.Password); err != nil {
|
||||||
|
c.AbortWithError(http.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := h.TokenVerifier.CreateToken(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusUnauthorized, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go h.Service.UpdateLastLogin(context.Background(), user.ID)
|
||||||
|
|
||||||
|
maxAge := (int)((expiration * time.Hour).Seconds())
|
||||||
|
c.SetCookie(authCookie, t, maxAge, "", "", false, true)
|
||||||
|
c.JSON(http.StatusOK, map[string]string{
|
||||||
|
"token": t,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) registerPost(c *gin.Context) {
|
||||||
|
email, _ := c.GetPostForm("email")
|
||||||
|
password, _ := c.GetPostForm("password")
|
||||||
|
name, _ := c.GetPostForm("name")
|
||||||
|
|
||||||
|
_, err := h.Service.GetUserByUsername(c.Request.Context(), email)
|
||||||
|
if err == nil {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := h.CredentialsVerifier.Hash(password)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createUser := postgres.CreateUserParams{
|
||||||
|
Name: name,
|
||||||
|
Password: hash,
|
||||||
|
Email: email,
|
||||||
|
}
|
||||||
|
_, err = h.Service.CreateUser(c.Request.Context(), createUser)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
}
|
45
http/templates.go
Normal file
45
http/templates.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/web"
|
||||||
|
"github.com/gin-gonic/gin/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Templates struct {
|
||||||
|
templates map[string]*template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTemplates(funcMap template.FuncMap) (*Templates, error) {
|
||||||
|
templates, err := fs.Glob(web.Templates, "*.tpl")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("glob: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &Templates{
|
||||||
|
templates: make(map[string]*template.Template, 0),
|
||||||
|
}
|
||||||
|
pages, err := fs.Glob(web.Templates, "*.html")
|
||||||
|
for _, page := range pages {
|
||||||
|
allTemplates := append(templates, page)
|
||||||
|
tpl, err := template.New(page).Funcs(funcMap).ParseFS(web.Templates, allTemplates...)
|
||||||
|
fmt.Printf("page: %s, templates: %v\n", page, templates)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.templates[page] = tpl
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tpl *Templates) Instance(name string, obj interface{}) render.Render {
|
||||||
|
return render.HTML{
|
||||||
|
Template: tpl.templates[name],
|
||||||
|
Name: name,
|
||||||
|
Data: obj,
|
||||||
|
}
|
||||||
|
}
|
62
http/transaction-edit.go
Normal file
62
http/transaction-edit.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransactionData struct {
|
||||||
|
AlwaysNeededData
|
||||||
|
Transaction *postgres.Transaction
|
||||||
|
Account *postgres.Account
|
||||||
|
Categories []postgres.GetCategoriesRow
|
||||||
|
Payees []postgres.Payee
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) transaction(c *gin.Context) {
|
||||||
|
data := c.MustGet("data").(AlwaysNeededData)
|
||||||
|
|
||||||
|
transactionID := c.Param("transactionid")
|
||||||
|
transactionUUID, err := uuid.Parse(transactionID)
|
||||||
|
if err != nil {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction, err := h.Service.GetTransaction(c.Request.Context(), transactionUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := h.Service.GetAccount(c.Request.Context(), transaction.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
categories, err := h.Service.GetCategories(c.Request.Context(), data.Budget.ID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payees, err := h.Service.GetPayees(c.Request.Context(), data.Budget.ID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d := TransactionData{
|
||||||
|
data,
|
||||||
|
&transaction,
|
||||||
|
&account,
|
||||||
|
categories,
|
||||||
|
payees,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "transaction.html", d)
|
||||||
|
}
|
98
http/transaction.go
Normal file
98
http/transaction.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) newTransaction(c *gin.Context) {
|
||||||
|
transactionMemo, _ := c.GetPostForm("memo")
|
||||||
|
transactionAccountID, err := getUUID(c, "account_id")
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotAcceptable, fmt.Errorf("account_id: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionCategoryID, err := getNullUUIDFromForm(c, "category_id")
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotAcceptable, fmt.Errorf("category_id: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionPayeeID, err := getNullUUIDFromForm(c, "payee_id")
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotAcceptable, fmt.Errorf("payee_id: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionDate, succ := c.GetPostForm("date")
|
||||||
|
if !succ {
|
||||||
|
c.AbortWithError(http.StatusNotAcceptable, fmt.Errorf("date missing"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionDateValue, err := time.Parse("2006-01-02", transactionDate)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotAcceptable, fmt.Errorf("date is not a valid date"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionAmount, succ := c.GetPostForm("amount")
|
||||||
|
if !succ {
|
||||||
|
c.AbortWithError(http.StatusNotAcceptable, fmt.Errorf("amount missing"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
amount := postgres.Numeric{}
|
||||||
|
amount.Set(transactionAmount)
|
||||||
|
|
||||||
|
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: transactionMemo,
|
||||||
|
Date: transactionDateValue,
|
||||||
|
Amount: amount,
|
||||||
|
AccountID: transactionAccountID,
|
||||||
|
PayeeID: transactionPayeeID,
|
||||||
|
CategoryID: transactionCategoryID,
|
||||||
|
}
|
||||||
|
_, 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: transactionMemo,
|
||||||
|
Date: transactionDateValue,
|
||||||
|
Amount: amount,
|
||||||
|
AccountID: transactionAccountID,
|
||||||
|
PayeeID: transactionPayeeID,
|
||||||
|
CategoryID: transactionCategoryID,
|
||||||
|
}
|
||||||
|
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
Normal file
56
http/util.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
|
}
|
66
http/ynab-import.go
Normal file
66
http/ynab-import.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
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.GetPostForm("budget_id")
|
||||||
|
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(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(assignments)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
105
jwt/login.go
Normal file
105
jwt/login.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.javil.eu/jacob1123/budgeteer"
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenVerifier verifies Tokens
|
||||||
|
type TokenVerifier struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token contains everything to authenticate a user
|
||||||
|
type Token struct {
|
||||||
|
username string
|
||||||
|
name string
|
||||||
|
expiry float64
|
||||||
|
id uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
expiration = 72
|
||||||
|
secret = "uditapbzuditagscwxuqdflgzpbu´ßiaefnlmzeßtrubiadern"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
"name": user.Name,
|
||||||
|
"exp": time.Now().Add(time.Hour * expiration).Unix(),
|
||||||
|
"id": user.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate encoded token and send it as response.
|
||||||
|
t, err := token.SignedString([]byte(secret))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyToken verifys a given string-token
|
||||||
|
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) {
|
||||||
|
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 []byte(secret), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse jwt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := verifyToken(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("verify jwt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn := &Token{
|
||||||
|
username: claims["usr"].(string),
|
||||||
|
name: claims["name"].(string),
|
||||||
|
expiry: claims["exp"].(float64),
|
||||||
|
id: uuid.MustParse(claims["id"].(string)),
|
||||||
|
}
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyToken(token *jwt.Token) (jwt.MapClaims, error) {
|
||||||
|
if !token.Valid {
|
||||||
|
return nil, fmt.Errorf("Token is not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Claims are not of Type MapClaims")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
|
||||||
|
return nil, fmt.Errorf("Claims have expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) GetName() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) GetUsername() string {
|
||||||
|
return t.username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) GetExpiry() float64 {
|
||||||
|
return t.expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) GetID() uuid.UUID {
|
||||||
|
return t.id
|
||||||
|
}
|
76
login.go
76
login.go
@ -1,76 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"gopkg.in/gin-gonic/gin.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
expiration = 72
|
|
||||||
secret = "uditapbzuditagscwxuqdflgzpbu´ßiaefnlmzeßtrubiadern"
|
|
||||||
authCookie = "authentication"
|
|
||||||
)
|
|
||||||
|
|
||||||
func verifyLogin(c *gin.Context) bool {
|
|
||||||
tokenString, err := c.Cookie(authCookie)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
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 []byte(secret), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if !verifyToken(c, token, err) {
|
|
||||||
c.SetCookie(authCookie, "", -1, "", "", false, false)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyToken(c *gin.Context, token *jwt.Token, err error) bool {
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
|
||||||
if !ok || !token.Valid {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func loginSuccess(c *gin.Context, username string, name string) {
|
|
||||||
// Create token
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
||||||
"usr": username,
|
|
||||||
"name": name,
|
|
||||||
"exp": time.Now().Add(time.Hour * expiration).Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generate encoded token and send it as response.
|
|
||||||
t, err := token.SignedString([]byte(secret))
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxAge := (int)((expiration * time.Hour).Seconds())
|
|
||||||
c.SetCookie(authCookie, t, maxAge, "", "", false, true)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, map[string]string{
|
|
||||||
"token": t,
|
|
||||||
})
|
|
||||||
}
|
|
71
main.go
71
main.go
@ -1,71 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"gopkg.in/gin-gonic/gin.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
router := gin.Default()
|
|
||||||
|
|
||||||
router.LoadHTMLGlob("./templates/*")
|
|
||||||
// Middleware
|
|
||||||
//e.Use(middleware.Logger())
|
|
||||||
//e.Use(middleware.Recover())
|
|
||||||
//e.Use(middleware.Static("static"))
|
|
||||||
|
|
||||||
router.GET("/login.html", login)
|
|
||||||
a := router.Group("/api/v1")
|
|
||||||
{
|
|
||||||
a.GET("/login", func(c *gin.Context) {
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, "/login.html")
|
|
||||||
})
|
|
||||||
a.POST("/login", loginPost)
|
|
||||||
|
|
||||||
// Unauthenticated routes
|
|
||||||
a.GET("/check", func(c *gin.Context) {
|
|
||||||
c.String(http.StatusOK, "Accessible")
|
|
||||||
})
|
|
||||||
a.GET("/hello", func(c *gin.Context) {
|
|
||||||
c.String(http.StatusOK, "Hello, World!")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restricted group
|
|
||||||
r := a.Group("/restricted")
|
|
||||||
{
|
|
||||||
//r.Use(middleware.JWT([]byte(secret)))
|
|
||||||
r.GET("", restricted)
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Run(":1323")
|
|
||||||
}
|
|
||||||
|
|
||||||
func restricted(c *gin.Context) {
|
|
||||||
//user, _ := c.Get("user") //.(*jwt.Token)
|
|
||||||
//name := user.Claims["name"].(string)
|
|
||||||
name := "jan"
|
|
||||||
c.String(http.StatusOK, "Welcome "+name+"!")
|
|
||||||
}
|
|
||||||
|
|
||||||
func login(c *gin.Context) {
|
|
||||||
if verifyLogin(c) {
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, "/api/v1/hello")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "login.html", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loginPost(c *gin.Context) {
|
|
||||||
username, _ := c.GetPostForm("username")
|
|
||||||
password, _ := c.GetPostForm("password")
|
|
||||||
|
|
||||||
if username != "jan" || password != "passwort" {
|
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loginSuccess(c, username, "Jan Bader")
|
|
||||||
}
|
|
130
postgres/accounts.sql.go
Normal file
130
postgres/accounts.sql.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: accounts.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createAccount = `-- name: CreateAccount :one
|
||||||
|
INSERT INTO accounts
|
||||||
|
(name, budget_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING id, budget_id, name, on_budget
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateAccountParams struct {
|
||||||
|
Name string
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createAccount, arg.Name, arg.BudgetID)
|
||||||
|
var i Account
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.BudgetID,
|
||||||
|
&i.Name,
|
||||||
|
&i.OnBudget,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAccount = `-- name: GetAccount :one
|
||||||
|
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget FROM accounts
|
||||||
|
WHERE accounts.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAccount(ctx context.Context, id uuid.UUID) (Account, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getAccount, id)
|
||||||
|
var i Account
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.BudgetID,
|
||||||
|
&i.Name,
|
||||||
|
&i.OnBudget,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAccounts = `-- name: GetAccounts :many
|
||||||
|
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget FROM accounts
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
ORDER BY accounts.name
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Account, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAccounts, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Account
|
||||||
|
for rows.Next() {
|
||||||
|
var i Account
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.BudgetID,
|
||||||
|
&i.Name,
|
||||||
|
&i.OnBudget,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many
|
||||||
|
SELECT accounts.id, accounts.name, accounts.on_budget, SUM(transactions.amount)::decimal(12,2) as balance
|
||||||
|
FROM accounts
|
||||||
|
LEFT JOIN transactions ON transactions.account_id = accounts.id
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
AND transactions.date < NOW()
|
||||||
|
GROUP BY accounts.id, accounts.name
|
||||||
|
ORDER BY accounts.name
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAccountsWithBalanceRow struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Name string
|
||||||
|
OnBudget bool
|
||||||
|
Balance Numeric
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID) ([]GetAccountsWithBalanceRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAccountsWithBalance, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetAccountsWithBalanceRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetAccountsWithBalanceRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.OnBudget,
|
||||||
|
&i.Balance,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
88
postgres/assignments.sql.go
Normal file
88
postgres/assignments.sql.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: assignments.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createAssignment = `-- name: CreateAssignment :one
|
||||||
|
INSERT INTO assignments (
|
||||||
|
date, amount, category_id
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3
|
||||||
|
)
|
||||||
|
RETURNING id, category_id, date, memo, amount
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateAssignmentParams struct {
|
||||||
|
Date time.Time
|
||||||
|
Amount Numeric
|
||||||
|
CategoryID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateAssignment(ctx context.Context, arg CreateAssignmentParams) (Assignment, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createAssignment, arg.Date, arg.Amount, arg.CategoryID)
|
||||||
|
var i Assignment
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.Date,
|
||||||
|
&i.Memo,
|
||||||
|
&i.Amount,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAllAssignments = `-- name: DeleteAllAssignments :execrows
|
||||||
|
DELETE FROM assignments
|
||||||
|
USING categories
|
||||||
|
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||||
|
WHERE categories.id = assignments.category_id AND category_groups.budget_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAllAssignments(ctx context.Context, budgetID uuid.UUID) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, deleteAllAssignments, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAssignmentsByMonthAndCategory = `-- name: GetAssignmentsByMonthAndCategory :many
|
||||||
|
SELECT date, category_id, budget_id, amount
|
||||||
|
FROM assignments_by_month
|
||||||
|
WHERE assignments_by_month.budget_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAssignmentsByMonthAndCategory(ctx context.Context, budgetID uuid.UUID) ([]AssignmentsByMonth, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getAssignmentsByMonthAndCategory, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []AssignmentsByMonth
|
||||||
|
for rows.Next() {
|
||||||
|
var i AssignmentsByMonth
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Date,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.BudgetID,
|
||||||
|
&i.Amount,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
125
postgres/budgets.sql.go
Normal file
125
postgres/budgets.sql.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: budgets.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createBudget = `-- name: CreateBudget :one
|
||||||
|
INSERT INTO budgets
|
||||||
|
(name, income_category_id, last_modification)
|
||||||
|
VALUES ($1, $2, NOW())
|
||||||
|
RETURNING id, name, last_modification, income_category_id
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateBudgetParams struct {
|
||||||
|
Name string
|
||||||
|
IncomeCategoryID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateBudget(ctx context.Context, arg CreateBudgetParams) (Budget, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createBudget, arg.Name, arg.IncomeCategoryID)
|
||||||
|
var i Budget
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.LastModification,
|
||||||
|
&i.IncomeCategoryID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBudget = `-- name: GetBudget :one
|
||||||
|
SELECT id, name, last_modification, income_category_id FROM budgets
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetBudget(ctx context.Context, id uuid.UUID) (Budget, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getBudget, id)
|
||||||
|
var i Budget
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.LastModification,
|
||||||
|
&i.IncomeCategoryID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBudgetsForUser = `-- name: GetBudgetsForUser :many
|
||||||
|
SELECT budgets.id, budgets.name, budgets.last_modification, budgets.income_category_id FROM budgets
|
||||||
|
LEFT JOIN user_budgets ON budgets.id = user_budgets.budget_id
|
||||||
|
WHERE user_budgets.user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetBudgetsForUser(ctx context.Context, userID uuid.UUID) ([]Budget, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getBudgetsForUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Budget
|
||||||
|
for rows.Next() {
|
||||||
|
var i Budget
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.LastModification,
|
||||||
|
&i.IncomeCategoryID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFirstActivity = `-- name: GetFirstActivity :one
|
||||||
|
SELECT MIN(dates.min_date)::date as min_date
|
||||||
|
FROM (
|
||||||
|
SELECT MIN(assignments.date) as min_date
|
||||||
|
FROM assignments
|
||||||
|
INNER JOIN categories ON categories.id = assignments.category_id
|
||||||
|
INNER JOIN category_groups ON category_groups.id = categories.category_group_id
|
||||||
|
WHERE category_groups.budget_id = $1
|
||||||
|
UNION
|
||||||
|
SELECT MIN(transactions.date) as min_date
|
||||||
|
FROM transactions
|
||||||
|
INNER JOIN accounts ON accounts.id = transactions.account_id
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
) dates
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetFirstActivity(ctx context.Context, budgetID uuid.UUID) (time.Time, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getFirstActivity, budgetID)
|
||||||
|
var min_date time.Time
|
||||||
|
err := row.Scan(&min_date)
|
||||||
|
return min_date, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const setInflowCategory = `-- name: SetInflowCategory :exec
|
||||||
|
UPDATE budgets
|
||||||
|
SET income_category_id = $1
|
||||||
|
WHERE budgets.id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetInflowCategoryParams struct {
|
||||||
|
IncomeCategoryID uuid.UUID
|
||||||
|
ID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetInflowCategory(ctx context.Context, arg SetInflowCategoryParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, setInflowCategory, arg.IncomeCategoryID, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
56
postgres/budgetservice.go
Normal file
56
postgres/budgetservice.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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{
|
||||||
|
Name: name,
|
||||||
|
IncomeCategoryID: uuid.New(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create budget: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ub := LinkBudgetToUserParams{UserID: userID, BudgetID: budget.ID}
|
||||||
|
_, err = q.LinkBudgetToUser(context, ub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("link budget to user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
group, err := q.CreateCategoryGroup(context, CreateCategoryGroupParams{
|
||||||
|
Name: "Inflow",
|
||||||
|
BudgetID: budget.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create inflow category_group: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cat, err := q.CreateCategory(context, CreateCategoryParams{
|
||||||
|
Name: "Ready to Assign",
|
||||||
|
CategoryGroupID: group.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create ready to assign category: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = q.SetInflowCategory(context, SetInflowCategoryParams{
|
||||||
|
IncomeCategoryID: cat.ID,
|
||||||
|
ID: budget.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("set inflow category: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
return &budget, nil
|
||||||
|
}
|
118
postgres/categories.sql.go
Normal file
118
postgres/categories.sql.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: categories.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createCategory = `-- name: CreateCategory :one
|
||||||
|
INSERT INTO categories
|
||||||
|
(name, category_group_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING id, category_group_id, name
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateCategoryParams struct {
|
||||||
|
Name string
|
||||||
|
CategoryGroupID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateCategory(ctx context.Context, arg CreateCategoryParams) (Category, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createCategory, arg.Name, arg.CategoryGroupID)
|
||||||
|
var i Category
|
||||||
|
err := row.Scan(&i.ID, &i.CategoryGroupID, &i.Name)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createCategoryGroup = `-- name: CreateCategoryGroup :one
|
||||||
|
INSERT INTO category_groups
|
||||||
|
(name, budget_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING id, budget_id, name
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateCategoryGroupParams struct {
|
||||||
|
Name string
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateCategoryGroup(ctx context.Context, arg CreateCategoryGroupParams) (CategoryGroup, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createCategoryGroup, arg.Name, arg.BudgetID)
|
||||||
|
var i CategoryGroup
|
||||||
|
err := row.Scan(&i.ID, &i.BudgetID, &i.Name)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategories = `-- name: GetCategories :many
|
||||||
|
SELECT categories.id, categories.category_group_id, categories.name, category_groups.name as group FROM categories
|
||||||
|
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||||
|
WHERE category_groups.budget_id = $1
|
||||||
|
ORDER BY category_groups.name, categories.name
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetCategoriesRow struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CategoryGroupID uuid.UUID
|
||||||
|
Name string
|
||||||
|
Group string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetCategories(ctx context.Context, budgetID uuid.UUID) ([]GetCategoriesRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getCategories, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetCategoriesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetCategoriesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CategoryGroupID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Group,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryGroups = `-- name: GetCategoryGroups :many
|
||||||
|
SELECT category_groups.id, category_groups.budget_id, category_groups.name FROM category_groups
|
||||||
|
WHERE category_groups.budget_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetCategoryGroups(ctx context.Context, budgetID uuid.UUID) ([]CategoryGroup, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getCategoryGroups, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []CategoryGroup
|
||||||
|
for rows.Next() {
|
||||||
|
var i CategoryGroup
|
||||||
|
if err := rows.Scan(&i.ID, &i.BudgetID, &i.Name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
37
postgres/conn.go
Normal file
37
postgres/conn.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/v4/stdlib"
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed schema/*.sql
|
||||||
|
var migrations embed.FS
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
*Queries
|
||||||
|
*sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to a database
|
||||||
|
func Connect(server string, user string, password string, database string) (*Database, error) {
|
||||||
|
connString := fmt.Sprintf("postgres://%s:%s@%s/%s", user, password, server, database)
|
||||||
|
conn, err := sql.Open("pgx", connString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
goose.SetBaseFS(migrations)
|
||||||
|
if err = goose.Up(conn, "schema"); err != nil {
|
||||||
|
return nil, fmt.Errorf("migrate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Database{
|
||||||
|
New(conn),
|
||||||
|
conn,
|
||||||
|
}, nil
|
||||||
|
}
|
60
postgres/cumultative-balances.sql.go
Normal file
60
postgres/cumultative-balances.sql.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: cumultative-balances.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getCumultativeBalances = `-- name: GetCumultativeBalances :many
|
||||||
|
SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id),
|
||||||
|
COALESCE(ass.amount, 0)::decimal(12,2) as assignments, SUM(ass.amount) OVER (PARTITION BY ass.category_id ORDER BY ass.date)::decimal(12,2) as assignments_cum,
|
||||||
|
COALESCE(tra.amount, 0)::decimal(12,2) as transactions, SUM(tra.amount) OVER (PARTITION BY tra.category_id ORDER BY tra.date)::decimal(12,2) as transactions_cum
|
||||||
|
FROM assignments_by_month as ass
|
||||||
|
FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id
|
||||||
|
WHERE (ass.budget_id IS NULL OR ass.budget_id = $1) AND (tra.budget_id IS NULL OR tra.budget_id = $1)
|
||||||
|
ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id)
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetCumultativeBalancesRow struct {
|
||||||
|
Date time.Time
|
||||||
|
CategoryID uuid.UUID
|
||||||
|
Assignments Numeric
|
||||||
|
AssignmentsCum Numeric
|
||||||
|
Transactions Numeric
|
||||||
|
TransactionsCum Numeric
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetCumultativeBalances(ctx context.Context, budgetID uuid.UUID) ([]GetCumultativeBalancesRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getCumultativeBalances, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetCumultativeBalancesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetCumultativeBalancesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Date,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.Assignments,
|
||||||
|
&i.AssignmentsCum,
|
||||||
|
&i.Transactions,
|
||||||
|
&i.TransactionsCum,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
29
postgres/db.go
Normal file
29
postgres/db.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||||
|
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||||
|
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||||
|
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
88
postgres/models.go
Normal file
88
postgres/models.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
Name string
|
||||||
|
OnBudget bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Assignment struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CategoryID uuid.UUID
|
||||||
|
Date time.Time
|
||||||
|
Memo sql.NullString
|
||||||
|
Amount Numeric
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssignmentsByMonth struct {
|
||||||
|
Date time.Time
|
||||||
|
CategoryID uuid.UUID
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
Amount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Budget struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Name string
|
||||||
|
LastModification sql.NullTime
|
||||||
|
IncomeCategoryID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
CategoryGroupID uuid.UUID
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryGroup struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payee struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Date time.Time
|
||||||
|
Memo string
|
||||||
|
Amount Numeric
|
||||||
|
AccountID uuid.UUID
|
||||||
|
CategoryID uuid.NullUUID
|
||||||
|
PayeeID uuid.NullUUID
|
||||||
|
GroupID uuid.NullUUID
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionsByMonth struct {
|
||||||
|
Date time.Time
|
||||||
|
CategoryID uuid.NullUUID
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
Amount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Email string
|
||||||
|
Name string
|
||||||
|
Password string
|
||||||
|
LastLogin sql.NullTime
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserBudget struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
}
|
35
postgres/numeric.go
Normal file
35
postgres/numeric.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import "github.com/jackc/pgtype"
|
||||||
|
|
||||||
|
type Numeric struct {
|
||||||
|
pgtype.Numeric
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Numeric) GetFloat64() float64 {
|
||||||
|
if n.Status != pgtype.Present {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var balance float64
|
||||||
|
err := n.AssignTo(&balance)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return balance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Numeric) IsPositive() bool {
|
||||||
|
if n.Status != pgtype.Present {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
float := n.GetFloat64()
|
||||||
|
return float >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Numeric) IsZero() bool {
|
||||||
|
if n.Status != pgtype.Present {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
float := n.GetFloat64()
|
||||||
|
return float == 0
|
||||||
|
}
|
58
postgres/payees.sql.go
Normal file
58
postgres/payees.sql.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: payees.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createPayee = `-- name: CreatePayee :one
|
||||||
|
INSERT INTO payees
|
||||||
|
(name, budget_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING id, budget_id, name
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreatePayeeParams struct {
|
||||||
|
Name string
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreatePayee(ctx context.Context, arg CreatePayeeParams) (Payee, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createPayee, arg.Name, arg.BudgetID)
|
||||||
|
var i Payee
|
||||||
|
err := row.Scan(&i.ID, &i.BudgetID, &i.Name)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPayees = `-- name: GetPayees :many
|
||||||
|
SELECT payees.id, payees.budget_id, payees.name FROM payees
|
||||||
|
WHERE payees.budget_id = $1
|
||||||
|
ORDER BY name
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPayees(ctx context.Context, budgetID uuid.UUID) ([]Payee, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getPayees, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Payee
|
||||||
|
for rows.Next() {
|
||||||
|
var i Payee
|
||||||
|
if err := rows.Scan(&i.ID, &i.BudgetID, &i.Name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
23
postgres/queries/accounts.sql
Normal file
23
postgres/queries/accounts.sql
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
-- name: CreateAccount :one
|
||||||
|
INSERT INTO accounts
|
||||||
|
(name, budget_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetAccount :one
|
||||||
|
SELECT accounts.* FROM accounts
|
||||||
|
WHERE accounts.id = $1;
|
||||||
|
|
||||||
|
-- name: GetAccounts :many
|
||||||
|
SELECT accounts.* FROM accounts
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
ORDER BY accounts.name;
|
||||||
|
|
||||||
|
-- name: GetAccountsWithBalance :many
|
||||||
|
SELECT accounts.id, accounts.name, accounts.on_budget, SUM(transactions.amount)::decimal(12,2) as balance
|
||||||
|
FROM accounts
|
||||||
|
LEFT JOIN transactions ON transactions.account_id = accounts.id
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
AND transactions.date < NOW()
|
||||||
|
GROUP BY accounts.id, accounts.name
|
||||||
|
ORDER BY accounts.name;
|
18
postgres/queries/assignments.sql
Normal file
18
postgres/queries/assignments.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
-- name: CreateAssignment :one
|
||||||
|
INSERT INTO assignments (
|
||||||
|
date, amount, category_id
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteAllAssignments :execrows
|
||||||
|
DELETE FROM assignments
|
||||||
|
USING categories
|
||||||
|
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||||
|
WHERE categories.id = assignments.category_id AND category_groups.budget_id = @budget_id;
|
||||||
|
|
||||||
|
-- name: GetAssignmentsByMonthAndCategory :many
|
||||||
|
SELECT *
|
||||||
|
FROM assignments_by_month
|
||||||
|
WHERE assignments_by_month.budget_id = @budget_id;
|
34
postgres/queries/budgets.sql
Normal file
34
postgres/queries/budgets.sql
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
-- name: CreateBudget :one
|
||||||
|
INSERT INTO budgets
|
||||||
|
(name, income_category_id, last_modification)
|
||||||
|
VALUES ($1, $2, NOW())
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: SetInflowCategory :exec
|
||||||
|
UPDATE budgets
|
||||||
|
SET income_category_id = $1
|
||||||
|
WHERE budgets.id = $2;
|
||||||
|
|
||||||
|
-- name: GetBudgetsForUser :many
|
||||||
|
SELECT budgets.* FROM budgets
|
||||||
|
LEFT JOIN user_budgets ON budgets.id = user_budgets.budget_id
|
||||||
|
WHERE user_budgets.user_id = $1;
|
||||||
|
|
||||||
|
-- name: GetBudget :one
|
||||||
|
SELECT * FROM budgets
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetFirstActivity :one
|
||||||
|
SELECT MIN(dates.min_date)::date as min_date
|
||||||
|
FROM (
|
||||||
|
SELECT MIN(assignments.date) as min_date
|
||||||
|
FROM assignments
|
||||||
|
INNER JOIN categories ON categories.id = assignments.category_id
|
||||||
|
INNER JOIN category_groups ON category_groups.id = categories.category_group_id
|
||||||
|
WHERE category_groups.budget_id = @budget_id
|
||||||
|
UNION
|
||||||
|
SELECT MIN(transactions.date) as min_date
|
||||||
|
FROM transactions
|
||||||
|
INNER JOIN accounts ON accounts.id = transactions.account_id
|
||||||
|
WHERE accounts.budget_id = @budget_id
|
||||||
|
) dates;
|
21
postgres/queries/categories.sql
Normal file
21
postgres/queries/categories.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-- name: CreateCategoryGroup :one
|
||||||
|
INSERT INTO category_groups
|
||||||
|
(name, budget_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetCategoryGroups :many
|
||||||
|
SELECT category_groups.* FROM category_groups
|
||||||
|
WHERE category_groups.budget_id = $1;
|
||||||
|
|
||||||
|
-- name: CreateCategory :one
|
||||||
|
INSERT INTO categories
|
||||||
|
(name, category_group_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetCategories :many
|
||||||
|
SELECT categories.*, category_groups.name as group FROM categories
|
||||||
|
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||||
|
WHERE category_groups.budget_id = $1
|
||||||
|
ORDER BY category_groups.name, categories.name;
|
8
postgres/queries/cumultative-balances.sql
Normal file
8
postgres/queries/cumultative-balances.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-- name: GetCumultativeBalances :many
|
||||||
|
SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id),
|
||||||
|
COALESCE(ass.amount, 0)::decimal(12,2) as assignments, SUM(ass.amount) OVER (PARTITION BY ass.category_id ORDER BY ass.date)::decimal(12,2) as assignments_cum,
|
||||||
|
COALESCE(tra.amount, 0)::decimal(12,2) as transactions, SUM(tra.amount) OVER (PARTITION BY tra.category_id ORDER BY tra.date)::decimal(12,2) as transactions_cum
|
||||||
|
FROM assignments_by_month as ass
|
||||||
|
FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id
|
||||||
|
WHERE (ass.budget_id IS NULL OR ass.budget_id = @budget_id) AND (tra.budget_id IS NULL OR tra.budget_id = @budget_id)
|
||||||
|
ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id);
|
10
postgres/queries/payees.sql
Normal file
10
postgres/queries/payees.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-- name: CreatePayee :one
|
||||||
|
INSERT INTO payees
|
||||||
|
(name, budget_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetPayees :many
|
||||||
|
SELECT payees.* FROM payees
|
||||||
|
WHERE payees.budget_id = $1
|
||||||
|
ORDER BY name;
|
58
postgres/queries/transactions.sql
Normal file
58
postgres/queries/transactions.sql
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
-- name: GetTransaction :one
|
||||||
|
SELECT * FROM transactions
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: CreateTransaction :one
|
||||||
|
INSERT INTO transactions
|
||||||
|
(date, memo, amount, account_id, payee_id, category_id, group_id)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateTransaction :exec
|
||||||
|
UPDATE transactions
|
||||||
|
SET date = $1,
|
||||||
|
memo = $2,
|
||||||
|
amount = $3,
|
||||||
|
account_id = $4,
|
||||||
|
payee_id = $5,
|
||||||
|
category_id = $6
|
||||||
|
WHERE id = $7;
|
||||||
|
|
||||||
|
-- name: DeleteTransaction :exec
|
||||||
|
DELETE FROM transactions
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetTransactionsForBudget :many
|
||||||
|
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id,
|
||||||
|
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category
|
||||||
|
FROM transactions
|
||||||
|
INNER JOIN accounts ON accounts.id = transactions.account_id
|
||||||
|
LEFT JOIN payees ON payees.id = transactions.payee_id
|
||||||
|
LEFT JOIN categories ON categories.id = transactions.category_id
|
||||||
|
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
ORDER BY transactions.date DESC
|
||||||
|
LIMIT 200;
|
||||||
|
|
||||||
|
-- name: GetTransactionsForAccount :many
|
||||||
|
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id,
|
||||||
|
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category
|
||||||
|
FROM transactions
|
||||||
|
INNER JOIN accounts ON accounts.id = transactions.account_id
|
||||||
|
LEFT JOIN payees ON payees.id = transactions.payee_id
|
||||||
|
LEFT JOIN categories ON categories.id = transactions.category_id
|
||||||
|
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
|
||||||
|
WHERE transactions.account_id = $1
|
||||||
|
ORDER BY transactions.date DESC
|
||||||
|
LIMIT 200;
|
||||||
|
|
||||||
|
-- name: DeleteAllTransactions :execrows
|
||||||
|
DELETE FROM transactions
|
||||||
|
USING accounts
|
||||||
|
WHERE accounts.budget_id = @budget_id
|
||||||
|
AND accounts.id = transactions.account_id;
|
||||||
|
|
||||||
|
-- name: GetTransactionsByMonthAndCategory :many
|
||||||
|
SELECT *
|
||||||
|
FROM transactions_by_month
|
||||||
|
WHERE transactions_by_month.budget_id = @budget_id;
|
5
postgres/queries/user_budgets.sql
Normal file
5
postgres/queries/user_budgets.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- name: LinkBudgetToUser :one
|
||||||
|
INSERT INTO user_budgets
|
||||||
|
(user_id, budget_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING *;
|
19
postgres/queries/users.sql
Normal file
19
postgres/queries/users.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- name: GetUserByUsername :one
|
||||||
|
SELECT * FROM users
|
||||||
|
WHERE email = $1;
|
||||||
|
|
||||||
|
-- name: GetUser :one
|
||||||
|
SELECT * FROM users
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: CreateUser :one
|
||||||
|
INSERT INTO users
|
||||||
|
(email, name, password)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateLastLogin :one
|
||||||
|
UPDATE users
|
||||||
|
SET last_login = NOW()
|
||||||
|
WHERE users.id = $1
|
||||||
|
RETURNING *;
|
5
postgres/schema/0001_enable-uuid-ossp.sql
Normal file
5
postgres/schema/0001_enable-uuid-ossp.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP EXTENSION "uuid-ossp";
|
9
postgres/schema/0002_budgets.sql
Normal file
9
postgres/schema/0002_budgets.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE budgets (
|
||||||
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
name text NOT NULL,
|
||||||
|
last_modification timestamp with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE budgets;
|
11
postgres/schema/0003_users.sql
Normal file
11
postgres/schema/0003_users.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE users (
|
||||||
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
email text NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
password text NOT NULL,
|
||||||
|
last_login timestamp with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE users;
|
8
postgres/schema/0004_user_budgets.sql
Normal file
8
postgres/schema/0004_user_budgets.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE user_budgets (
|
||||||
|
user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE user_budgets;
|
10
postgres/schema/0005_accounts.sql
Normal file
10
postgres/schema/0005_accounts.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE accounts (
|
||||||
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE,
|
||||||
|
name varchar(50) NOT NULL,
|
||||||
|
on_budget boolean DEFAULT TRUE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE accounts;
|
9
postgres/schema/0006_payees.sql
Normal file
9
postgres/schema/0006_payees.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE payees (
|
||||||
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE,
|
||||||
|
name varchar(50) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE payees;
|
9
postgres/schema/0007_category-groups.sql
Normal file
9
postgres/schema/0007_category-groups.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE category_groups (
|
||||||
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE,
|
||||||
|
name varchar(50) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE category_groups;
|
12
postgres/schema/0008_categories.sql
Normal file
12
postgres/schema/0008_categories.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE categories (
|
||||||
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
category_group_id uuid NOT NULL REFERENCES category_groups (id) ON DELETE CASCADE,
|
||||||
|
name varchar(50) NOT NULL
|
||||||
|
);
|
||||||
|
ALTER TABLE budgets ADD COLUMN
|
||||||
|
income_category_id uuid NOT NULL REFERENCES categories (id) DEFERRABLE INITIALLY DEFERRED;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE budgets DROP COLUMN income_category_id;
|
||||||
|
DROP TABLE categories;
|
16
postgres/schema/0009_transactions.sql
Normal file
16
postgres/schema/0009_transactions.sql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE transactions (
|
||||||
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
date date NOT NULL,
|
||||||
|
memo text NOT NULL,
|
||||||
|
amount decimal(12,2) NOT NULL,
|
||||||
|
account_id uuid NOT NULL REFERENCES accounts (id),
|
||||||
|
category_id uuid REFERENCES categories (id),
|
||||||
|
payee_id uuid REFERENCES payees (id)
|
||||||
|
);
|
||||||
|
ALTER TABLE "transactions" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id");
|
||||||
|
ALTER TABLE "transactions" ADD FOREIGN KEY ("payee_id") REFERENCES "payees" ("id");
|
||||||
|
ALTER TABLE "transactions" ADD FOREIGN KEY ("category_id") REFERENCES "categories" ("id");
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE transactions;
|
11
postgres/schema/0010_assignments.sql
Normal file
11
postgres/schema/0010_assignments.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE assignments (
|
||||||
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
category_id uuid NOT NULL REFERENCES categories (id),
|
||||||
|
date date NOT NULL,
|
||||||
|
memo text,
|
||||||
|
amount decimal(12,2) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE assignments;
|
17
postgres/schema/0011_views-for-months.sql
Normal file
17
postgres/schema/0011_views-for-months.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE VIEW transactions_by_month AS
|
||||||
|
SELECT date_trunc('month', transactions.date)::date as date, transactions.category_id, accounts.budget_id, SUM(amount) as amount
|
||||||
|
FROM transactions
|
||||||
|
INNER JOIN accounts ON accounts.id = transactions.account_id
|
||||||
|
GROUP BY date_trunc('month', transactions.date), transactions.category_id, accounts.budget_id;
|
||||||
|
|
||||||
|
CREATE VIEW assignments_by_month AS
|
||||||
|
SELECT date_trunc('month', assignments.date)::date as date, assignments.category_id, category_groups.budget_id, SUM(amount) as amount
|
||||||
|
FROM assignments
|
||||||
|
INNER JOIN categories ON categories.id = assignments.category_id
|
||||||
|
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||||
|
GROUP BY date_trunc('month', assignments.date), assignments.category_id, category_groups.budget_id;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP VIEW transactions_by_month;
|
||||||
|
DROP VIEW assignments_by_month;
|
5
postgres/schema/0012_add-group-id.sql
Normal file
5
postgres/schema/0012_add-group-id.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE transactions ADD COLUMN group_id uuid NULL;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE transactions DROP COLUMN group_id;
|
282
postgres/transactions.sql.go
Normal file
282
postgres/transactions.sql.go
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: transactions.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createTransaction = `-- name: CreateTransaction :one
|
||||||
|
INSERT INTO transactions
|
||||||
|
(date, memo, amount, account_id, payee_id, category_id, group_id)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING id, date, memo, amount, account_id, category_id, payee_id, group_id
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateTransactionParams struct {
|
||||||
|
Date time.Time
|
||||||
|
Memo string
|
||||||
|
Amount Numeric
|
||||||
|
AccountID uuid.UUID
|
||||||
|
PayeeID uuid.NullUUID
|
||||||
|
CategoryID uuid.NullUUID
|
||||||
|
GroupID uuid.NullUUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (Transaction, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createTransaction,
|
||||||
|
arg.Date,
|
||||||
|
arg.Memo,
|
||||||
|
arg.Amount,
|
||||||
|
arg.AccountID,
|
||||||
|
arg.PayeeID,
|
||||||
|
arg.CategoryID,
|
||||||
|
arg.GroupID,
|
||||||
|
)
|
||||||
|
var i Transaction
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Date,
|
||||||
|
&i.Memo,
|
||||||
|
&i.Amount,
|
||||||
|
&i.AccountID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.PayeeID,
|
||||||
|
&i.GroupID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAllTransactions = `-- name: DeleteAllTransactions :execrows
|
||||||
|
DELETE FROM transactions
|
||||||
|
USING accounts
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
AND accounts.id = transactions.account_id
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAllTransactions(ctx context.Context, budgetID uuid.UUID) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, deleteAllTransactions, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteTransaction = `-- name: DeleteTransaction :exec
|
||||||
|
DELETE FROM transactions
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteTransaction(ctx context.Context, id uuid.UUID) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteTransaction, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTransaction = `-- name: GetTransaction :one
|
||||||
|
SELECT id, date, memo, amount, account_id, category_id, payee_id, group_id FROM transactions
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetTransaction(ctx context.Context, id uuid.UUID) (Transaction, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getTransaction, id)
|
||||||
|
var i Transaction
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Date,
|
||||||
|
&i.Memo,
|
||||||
|
&i.Amount,
|
||||||
|
&i.AccountID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.PayeeID,
|
||||||
|
&i.GroupID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTransactionsByMonthAndCategory = `-- name: GetTransactionsByMonthAndCategory :many
|
||||||
|
SELECT date, category_id, budget_id, amount
|
||||||
|
FROM transactions_by_month
|
||||||
|
WHERE transactions_by_month.budget_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetTransactionsByMonthAndCategory(ctx context.Context, budgetID uuid.UUID) ([]TransactionsByMonth, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getTransactionsByMonthAndCategory, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []TransactionsByMonth
|
||||||
|
for rows.Next() {
|
||||||
|
var i TransactionsByMonth
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Date,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.BudgetID,
|
||||||
|
&i.Amount,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many
|
||||||
|
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id,
|
||||||
|
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category
|
||||||
|
FROM transactions
|
||||||
|
INNER JOIN accounts ON accounts.id = transactions.account_id
|
||||||
|
LEFT JOIN payees ON payees.id = transactions.payee_id
|
||||||
|
LEFT JOIN categories ON categories.id = transactions.category_id
|
||||||
|
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
|
||||||
|
WHERE transactions.account_id = $1
|
||||||
|
ORDER BY transactions.date DESC
|
||||||
|
LIMIT 200
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetTransactionsForAccountRow struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Date time.Time
|
||||||
|
Memo string
|
||||||
|
Amount Numeric
|
||||||
|
GroupID uuid.NullUUID
|
||||||
|
Account string
|
||||||
|
Payee string
|
||||||
|
CategoryGroup string
|
||||||
|
Category string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.UUID) ([]GetTransactionsForAccountRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getTransactionsForAccount, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetTransactionsForAccountRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetTransactionsForAccountRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Date,
|
||||||
|
&i.Memo,
|
||||||
|
&i.Amount,
|
||||||
|
&i.GroupID,
|
||||||
|
&i.Account,
|
||||||
|
&i.Payee,
|
||||||
|
&i.CategoryGroup,
|
||||||
|
&i.Category,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTransactionsForBudget = `-- name: GetTransactionsForBudget :many
|
||||||
|
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id,
|
||||||
|
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category
|
||||||
|
FROM transactions
|
||||||
|
INNER JOIN accounts ON accounts.id = transactions.account_id
|
||||||
|
LEFT JOIN payees ON payees.id = transactions.payee_id
|
||||||
|
LEFT JOIN categories ON categories.id = transactions.category_id
|
||||||
|
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
ORDER BY transactions.date DESC
|
||||||
|
LIMIT 200
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetTransactionsForBudgetRow struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Date time.Time
|
||||||
|
Memo string
|
||||||
|
Amount Numeric
|
||||||
|
GroupID uuid.NullUUID
|
||||||
|
Account string
|
||||||
|
Payee string
|
||||||
|
CategoryGroup string
|
||||||
|
Category string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetTransactionsForBudget(ctx context.Context, budgetID uuid.UUID) ([]GetTransactionsForBudgetRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getTransactionsForBudget, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetTransactionsForBudgetRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetTransactionsForBudgetRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Date,
|
||||||
|
&i.Memo,
|
||||||
|
&i.Amount,
|
||||||
|
&i.GroupID,
|
||||||
|
&i.Account,
|
||||||
|
&i.Payee,
|
||||||
|
&i.CategoryGroup,
|
||||||
|
&i.Category,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTransaction = `-- name: UpdateTransaction :exec
|
||||||
|
UPDATE transactions
|
||||||
|
SET date = $1,
|
||||||
|
memo = $2,
|
||||||
|
amount = $3,
|
||||||
|
account_id = $4,
|
||||||
|
payee_id = $5,
|
||||||
|
category_id = $6
|
||||||
|
WHERE id = $7
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateTransactionParams struct {
|
||||||
|
Date time.Time
|
||||||
|
Memo string
|
||||||
|
Amount Numeric
|
||||||
|
AccountID uuid.UUID
|
||||||
|
PayeeID uuid.NullUUID
|
||||||
|
CategoryID uuid.NullUUID
|
||||||
|
ID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateTransaction(ctx context.Context, arg UpdateTransactionParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateTransaction,
|
||||||
|
arg.Date,
|
||||||
|
arg.Memo,
|
||||||
|
arg.Amount,
|
||||||
|
arg.AccountID,
|
||||||
|
arg.PayeeID,
|
||||||
|
arg.CategoryID,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
29
postgres/user_budgets.sql.go
Normal file
29
postgres/user_budgets.sql.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: user_budgets.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const linkBudgetToUser = `-- name: LinkBudgetToUser :one
|
||||||
|
INSERT INTO user_budgets
|
||||||
|
(user_id, budget_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING user_id, budget_id
|
||||||
|
`
|
||||||
|
|
||||||
|
type LinkBudgetToUserParams struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) LinkBudgetToUser(ctx context.Context, arg LinkBudgetToUserParams) (UserBudget, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, linkBudgetToUser, arg.UserID, arg.BudgetID)
|
||||||
|
var i UserBudget
|
||||||
|
err := row.Scan(&i.UserID, &i.BudgetID)
|
||||||
|
return i, err
|
||||||
|
}
|
92
postgres/users.sql.go
Normal file
92
postgres/users.sql.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// source: users.sql
|
||||||
|
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createUser = `-- name: CreateUser :one
|
||||||
|
INSERT INTO users
|
||||||
|
(email, name, password)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, email, name, password, last_login
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateUserParams struct {
|
||||||
|
Email string
|
||||||
|
Name string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createUser, arg.Email, arg.Name, arg.Password)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Name,
|
||||||
|
&i.Password,
|
||||||
|
&i.LastLogin,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUser = `-- name: GetUser :one
|
||||||
|
SELECT id, email, name, password, last_login FROM users
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUser(ctx context.Context, id uuid.UUID) (User, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUser, id)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Name,
|
||||||
|
&i.Password,
|
||||||
|
&i.LastLogin,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||||
|
SELECT id, email, name, password, last_login FROM users
|
||||||
|
WHERE email = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByUsername(ctx context.Context, email string) (User, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserByUsername, email)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Name,
|
||||||
|
&i.Password,
|
||||||
|
&i.LastLogin,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateLastLogin = `-- name: UpdateLastLogin :one
|
||||||
|
UPDATE users
|
||||||
|
SET last_login = NOW()
|
||||||
|
WHERE users.id = $1
|
||||||
|
RETURNING id, email, name, password, last_login
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) UpdateLastLogin(ctx context.Context, id uuid.UUID) (User, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateLastLogin, id)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Name,
|
||||||
|
&i.Password,
|
||||||
|
&i.LastLogin,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
374
postgres/ynab-import.go
Normal file
374
postgres/ynab-import.go
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YNABImport struct {
|
||||||
|
Context context.Context
|
||||||
|
accounts []Account
|
||||||
|
payees []Payee
|
||||||
|
categories []GetCategoriesRow
|
||||||
|
categoryGroups []CategoryGroup
|
||||||
|
queries *Queries
|
||||||
|
budgetID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYNABImport(context context.Context, q *Queries, budgetID uuid.UUID) (*YNABImport, error) {
|
||||||
|
accounts, err := q.GetAccounts(context, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payees, err := q.GetPayees(context, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
categories, err := q.GetCategories(context, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryGroups, err := q.GetCategoryGroups(context, budgetID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &YNABImport{
|
||||||
|
Context: context,
|
||||||
|
accounts: accounts,
|
||||||
|
payees: payees,
|
||||||
|
categories: categories,
|
||||||
|
categoryGroups: categoryGroups,
|
||||||
|
queries: q,
|
||||||
|
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€
|
||||||
|
//
|
||||||
|
// Activity and Available are not imported, since they are determined by the transactions and historic assignments
|
||||||
|
func (ynab *YNABImport) ImportAssignments(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryGroup, categoryName := record[2], record[3] //also in 1 joined by :
|
||||||
|
category, err := ynab.GetCategory(categoryGroup, categoryName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount.Int.Int64() == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
assignment := CreateAssignmentParams{
|
||||||
|
Date: date,
|
||||||
|
CategoryID: category.UUID,
|
||||||
|
Amount: amount,
|
||||||
|
}
|
||||||
|
_, err = ynab.queries.CreateAssignment(ynab.Context, assignment)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not save assignment %v: %w", assignment, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Imported %d assignments\n", count)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transfer struct {
|
||||||
|
CreateTransactionParams
|
||||||
|
TransferToAccount *Account
|
||||||
|
FromAccount string
|
||||||
|
ToAccount string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportTransactions expects a TSV-file as exported by YNAB in the following format:
|
||||||
|
|
||||||
|
func (ynab *YNABImport) ImportTransactions(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var openTransfers []Transfer
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for _, record := range csvData[1:] {
|
||||||
|
accountName := record[0]
|
||||||
|
account, err := ynab.GetAccount(accountName)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction := CreateTransactionParams{
|
||||||
|
Date: date,
|
||||||
|
Memo: memo,
|
||||||
|
AccountID: account.ID,
|
||||||
|
CategoryID: category,
|
||||||
|
Amount: amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
payeeName := record[3]
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//status := record[10]
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not save transaction %v: %w", openTransfer.CreateTransactionParams, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
fmt.Printf("Imported %d transactions\n", count)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimLastChar(s string) string {
|
||||||
|
r, size := utf8.DecodeLastRuneInString(s)
|
||||||
|
if r == utf8.RuneError && (size == 0 || size == 1) {
|
||||||
|
size = 0
|
||||||
|
}
|
||||||
|
return s[:len(s)-size]
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAmount(inflow string, outflow string) (Numeric, error) {
|
||||||
|
// Remove trailing currency
|
||||||
|
inflow = strings.Replace(trimLastChar(inflow), ",", ".", 1)
|
||||||
|
outflow = strings.Replace(trimLastChar(outflow), ",", ".", 1)
|
||||||
|
|
||||||
|
num := Numeric{}
|
||||||
|
err := num.Set(inflow)
|
||||||
|
if err != nil {
|
||||||
|
return num, fmt.Errorf("Could not parse inflow %s: %w", inflow, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if inflow is zero, use outflow
|
||||||
|
if num.Int.Int64() != 0 {
|
||||||
|
return num, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = num.Set("-" + outflow)
|
||||||
|
if err != nil {
|
||||||
|
return num, fmt.Errorf("Could not parse outflow %s: %w", inflow, err)
|
||||||
|
}
|
||||||
|
return num, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ynab *YNABImport) GetAccount(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})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ynab.accounts = append(ynab.accounts, account)
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ynab *YNABImport) GetPayee(name string) (uuid.NullUUID, error) {
|
||||||
|
if name == "" {
|
||||||
|
return uuid.NullUUID{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pay := range ynab.payees {
|
||||||
|
if pay.Name == name {
|
||||||
|
return uuid.NullUUID{UUID: pay.ID, Valid: true}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payee, err := ynab.queries.CreatePayee(ynab.Context, CreatePayeeParams{Name: name, BudgetID: ynab.budgetID})
|
||||||
|
if err != nil {
|
||||||
|
return uuid.NullUUID{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ynab.payees = append(ynab.payees, payee)
|
||||||
|
return uuid.NullUUID{UUID: payee.ID, Valid: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ynab *YNABImport) GetCategory(group string, name string) (uuid.NullUUID, error) {
|
||||||
|
if group == "" || name == "" {
|
||||||
|
return uuid.NullUUID{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, category := range ynab.categories {
|
||||||
|
if category.Name == name && category.Group == group {
|
||||||
|
return uuid.NullUUID{UUID: category.ID, Valid: true}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryGroup, err := ynab.queries.CreateCategoryGroup(ynab.Context, CreateCategoryGroupParams{Name: group, BudgetID: ynab.budgetID})
|
||||||
|
if err != nil {
|
||||||
|
return uuid.NullUUID{}, err
|
||||||
|
}
|
||||||
|
ynab.categoryGroups = append(ynab.categoryGroups, categoryGroup)
|
||||||
|
|
||||||
|
category, err := ynab.queries.CreateCategory(ynab.Context, CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID})
|
||||||
|
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
|
||||||
|
}
|
15
sqlc.yaml
Normal file
15
sqlc.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
version: 1
|
||||||
|
packages:
|
||||||
|
- path: "postgres"
|
||||||
|
name: "postgres"
|
||||||
|
engine: "postgresql"
|
||||||
|
schema: "postgres/schema/"
|
||||||
|
queries: "postgres/queries/"
|
||||||
|
overrides:
|
||||||
|
- go_type:
|
||||||
|
type: "Numeric"
|
||||||
|
db_type: "pg_catalog.numeric"
|
||||||
|
- go_type:
|
||||||
|
type: "Numeric"
|
||||||
|
db_type: "pg_catalog.numeric"
|
||||||
|
nullable: true
|
@ -1,373 +0,0 @@
|
|||||||
/*! HTML5 Boilerplate v5.0 | MIT License | http://h5bp.com/ */
|
|
||||||
|
|
||||||
html {
|
|
||||||
color: #222;
|
|
||||||
font-size: 1em;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-moz-selection {
|
|
||||||
background: #b3d4fc;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: #b3d4fc;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
display: block;
|
|
||||||
height: 1px;
|
|
||||||
border: 0;
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
margin: 1em 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio,
|
|
||||||
canvas,
|
|
||||||
iframe,
|
|
||||||
img,
|
|
||||||
svg,
|
|
||||||
video {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset {
|
|
||||||
border: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browserupgrade {
|
|
||||||
margin: 0.2em 0;
|
|
||||||
background: #ccc;
|
|
||||||
color: #000;
|
|
||||||
padding: 0.2em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ===== Initializr Styles ==================================================
|
|
||||||
Author: Jonathan Verrecchia - verekia.com/initializr/responsive-template
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
body {
|
|
||||||
font: 16px/26px Helvetica, Helvetica Neue, Arial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
width: 90%;
|
|
||||||
margin: 0 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================
|
|
||||||
ALL: Orange Theme
|
|
||||||
=================== */
|
|
||||||
|
|
||||||
.header-container {
|
|
||||||
border-bottom: 20px solid #e44d26;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-container,
|
|
||||||
.main aside {
|
|
||||||
border-top: 20px solid #e44d26;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-container,
|
|
||||||
.footer-container,
|
|
||||||
.main aside {
|
|
||||||
background: #f16529;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==============
|
|
||||||
MOBILE: Menu
|
|
||||||
============== */
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 15px 0;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
background: #e44d26;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a:hover,
|
|
||||||
nav a:visited {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==============
|
|
||||||
MOBILE: Main
|
|
||||||
============== */
|
|
||||||
|
|
||||||
.main {
|
|
||||||
padding: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main article h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main aside {
|
|
||||||
color: white;
|
|
||||||
padding: 0px 5% 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-container footer {
|
|
||||||
color: white;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===============
|
|
||||||
ALL: IE Fixes
|
|
||||||
=============== */
|
|
||||||
|
|
||||||
.ie7 .title {
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
Author's custom styles
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
Media Queries
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
@media only screen and (min-width: 480px) {
|
|
||||||
|
|
||||||
/* ====================
|
|
||||||
INTERMEDIATE: Menu
|
|
||||||
==================== */
|
|
||||||
|
|
||||||
nav a {
|
|
||||||
float: left;
|
|
||||||
width: 27%;
|
|
||||||
margin: 0 1.7%;
|
|
||||||
padding: 25px 2%;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav li:first-child a {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav li:last-child a {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========================
|
|
||||||
INTERMEDIATE: IE Fixes
|
|
||||||
======================== */
|
|
||||||
|
|
||||||
nav ul li {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oldie nav a {
|
|
||||||
margin: 0 0.7%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 768px) {
|
|
||||||
|
|
||||||
/* ====================
|
|
||||||
WIDE: CSS3 Effects
|
|
||||||
==================== */
|
|
||||||
|
|
||||||
.header-container,
|
|
||||||
.main aside {
|
|
||||||
-webkit-box-shadow: 0 5px 10px #aaa;
|
|
||||||
-moz-box-shadow: 0 5px 10px #aaa;
|
|
||||||
box-shadow: 0 5px 10px #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============
|
|
||||||
WIDE: Menu
|
|
||||||
============ */
|
|
||||||
|
|
||||||
.title {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
float: right;
|
|
||||||
width: 38%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============
|
|
||||||
WIDE: Main
|
|
||||||
============ */
|
|
||||||
|
|
||||||
.main article {
|
|
||||||
float: left;
|
|
||||||
width: 57%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main aside {
|
|
||||||
float: right;
|
|
||||||
width: 28%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 1140px) {
|
|
||||||
|
|
||||||
/* ===============
|
|
||||||
Maximal Width
|
|
||||||
=============== */
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
width: 1026px; /* 1140px - 10% for margins */
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
Helper classes
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none !important;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visuallyhidden {
|
|
||||||
border: 0;
|
|
||||||
clip: rect(0 0 0 0);
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0;
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visuallyhidden.focusable:active,
|
|
||||||
.visuallyhidden.focusable:focus {
|
|
||||||
clip: auto;
|
|
||||||
height: auto;
|
|
||||||
margin: 0;
|
|
||||||
overflow: visible;
|
|
||||||
position: static;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invisible {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clearfix:before,
|
|
||||||
.clearfix:after {
|
|
||||||
content: " ";
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clearfix:after {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clearfix {
|
|
||||||
*zoom: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
Print styles
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
*,
|
|
||||||
*:before,
|
|
||||||
*:after {
|
|
||||||
background: transparent !important;
|
|
||||||
color: #000 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
text-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
a:visited {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href]:after {
|
|
||||||
content: " (" attr(href) ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
abbr[title]:after {
|
|
||||||
content: " (" attr(title) ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href^="#"]:after,
|
|
||||||
a[href^="javascript:"]:after {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pre,
|
|
||||||
blockquote {
|
|
||||||
border: 1px solid #999;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead {
|
|
||||||
display: table-header-group;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr,
|
|
||||||
img {
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
orphans: 3;
|
|
||||||
widows: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
page-break-after: avoid;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang=""> <![endif]-->
|
|
||||||
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang=""> <![endif]-->
|
|
||||||
<!--[if IE 8]> <html class="no-js lt-ie9" lang=""> <![endif]-->
|
|
||||||
<!--[if gt IE 8]><!--> <html class="no-js" lang=""> <!--<![endif]-->
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
|
||||||
<title>Budgeteer</title>
|
|
||||||
<meta name="description" content="">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="css/normalize.min.css">
|
|
||||||
<link rel="stylesheet" href="css/main.css">
|
|
||||||
|
|
||||||
<!--[if lt IE 9]>
|
|
||||||
<script src="js/vendor/html5-3.6-respond-1.4.2.min.js"></script>
|
|
||||||
<![endif]-->
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!--[if lt IE 8]>
|
|
||||||
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
|
||||||
<![endif]-->
|
|
||||||
|
|
||||||
<div class="header-container">
|
|
||||||
<header class="wrapper clearfix">
|
|
||||||
<h1 class="title">Budgeteer</h1>
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li><a href="#">Home</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-container">
|
|
||||||
<div class="main wrapper clearfix">
|
|
||||||
<form id="login-form" method="POST" action="/api/login">
|
|
||||||
<div class="form-group row">
|
|
||||||
<label for="username" class="col-sm-2 form-control-label">Username</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input type="text" class="form-control" id="username" name="username" placeholder="Username">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group row">
|
|
||||||
<label for="password" class="col-sm-2 form-control-label">Password</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group row">
|
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
|
||||||
<button type="submit" class="btn btn-secondary">Sign in</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div> <!-- #main -->
|
|
||||||
</div> <!-- #main-container -->
|
|
||||||
|
|
||||||
<div class="footer-container">
|
|
||||||
<footer class="wrapper">
|
|
||||||
<h3>footer</h3>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
|
|
||||||
<script>window.jQuery || document.write('<script src="js/vendor/jquery-1.11.2.min.js"><\/script>')</script>
|
|
||||||
|
|
||||||
<script src="js/plugins.js"></script>
|
|
||||||
<script src="js/main.js"></script>
|
|
||||||
|
|
||||||
<!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
|
|
||||||
<script>
|
|
||||||
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
|
|
||||||
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
|
|
||||||
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
|
|
||||||
e.src='//www.google-analytics.com/analytics.js';
|
|
||||||
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
|
|
||||||
ga('create','UA-XXXXX-X','auto');ga('send','pageview');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,14 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head><title>Login</title>
|
|
||||||
<body>
|
|
||||||
<form action="/api/v1/login?target=/" method="POST">
|
|
||||||
<label for="username">User</label>
|
|
||||||
<input type="text" name="username" /><br />
|
|
||||||
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<input type="password" name="password" /><br />
|
|
||||||
|
|
||||||
<input type="submit">Login</form>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
20
token.go
Normal file
20
token.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package budgeteer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Token contains data that authenticates a user
|
||||||
|
type Token interface {
|
||||||
|
GetUsername() string
|
||||||
|
GetName() string
|
||||||
|
GetExpiry() float64
|
||||||
|
GetID() uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenVerifier verifies a Token
|
||||||
|
type TokenVerifier interface {
|
||||||
|
VerifyToken(string) (Token, error)
|
||||||
|
CreateToken(*postgres.User) (string, error)
|
||||||
|
}
|
39
web/account.html
Normal file
39
web/account.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}{{.Account.Name}}{{end}}
|
||||||
|
|
||||||
|
{{define "new"}}
|
||||||
|
{{template "transaction-new" .}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<div class="budget-item">
|
||||||
|
<a href="#newtransactionmodal" data-bs-toggle="modal" data-bs-target="#newtransactionmodal">New Transaction</a>
|
||||||
|
<span class="time"></span>
|
||||||
|
</div>
|
||||||
|
<table class="container col-lg-12" id="content">
|
||||||
|
{{range .Transactions}}
|
||||||
|
<tr class="{{if .Date.After now}}future{{end}}">
|
||||||
|
<td>{{.Date.Format "02.01.2006"}}</td>
|
||||||
|
<td>
|
||||||
|
{{.Account}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{.Payee}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if .CategoryGroup}}
|
||||||
|
{{.CategoryGroup}} : {{.Category}}
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if .GroupID.Valid}}☀{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/budget/{{$.Budget.ID}}/transaction/{{.ID}}">{{.Memo}}</a>
|
||||||
|
</td>
|
||||||
|
{{template "amount-cell" .Amount}}
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
{{end}}
|
17
web/accounts.html
Normal file
17
web/accounts.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{{define "title"}}
|
||||||
|
Accounts
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "new"}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
{{range .Accounts}}
|
||||||
|
<div class="budget-item">
|
||||||
|
<a href="/budget/{{$.Budget.ID}}/account/{{.ID}}">{{.Name}}</a>
|
||||||
|
<span class="time">{{printf "%.2f" .Balance.GetFloat64}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
18
web/admin.html
Normal file
18
web/admin.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
{{define "title"}}
|
||||||
|
Admin
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "sidebar"}}
|
||||||
|
Settings for all Budgets
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<h1>Danger Zone</h1>
|
||||||
|
<div class="budget-item">
|
||||||
|
<button>Clear database</button>
|
||||||
|
<p>This removes all data and starts from scratch. Not undoable!</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
23
web/amount.tpl
Normal file
23
web/amount.tpl
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{{define "amount"}}
|
||||||
|
<span class="right {{if .IsZero}}zero{{else if not .IsPositive}}negative{{end}}">
|
||||||
|
{{printf "%.2f" .GetFloat64}}
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "amount-cell"}}
|
||||||
|
<td class="right {{if .IsZero}}zero{{else if not .IsPositive}}negative{{end}}">
|
||||||
|
{{printf "%.2f" .GetFloat64}}
|
||||||
|
</td>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "amountf64"}}
|
||||||
|
<span class="right {{if eq . 0.0}}zero{{else if lt . 0.0}}negative{{end}}">
|
||||||
|
{{printf "%.2f" .}}
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "amountf64-cell"}}
|
||||||
|
<td class="right {{if eq . 0.0}}zero{{else if lt . 0.0}}negative{{end}}">
|
||||||
|
{{printf "%.2f" .}}
|
||||||
|
</td>
|
||||||
|
{{end}}
|
39
web/base.tpl
Normal file
39
web/base.tpl
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{{define "base"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
|
||||||
|
<link href="/static/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="/static/css/main.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
<script src="https://malsup.github.io/jquery.form.js"></script>
|
||||||
|
|
||||||
|
<script src="/static/js/bootstrap.min.js"></script>
|
||||||
|
<script src="/static/js/main.js"></script>
|
||||||
|
<title>{{template "title" .}} - Budgeteer</title>
|
||||||
|
|
||||||
|
{{block "more-head" .}}{{end}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="wrapper">
|
||||||
|
<div id="sidebar">
|
||||||
|
{{block "sidebar" .}}
|
||||||
|
{{template "budget-sidebar" .}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div id="content">
|
||||||
|
<div class="container" id="head">
|
||||||
|
{{template "title" .}}
|
||||||
|
</div>
|
||||||
|
<div class="container col-lg-12" id="content">
|
||||||
|
{{template "main" .}}
|
||||||
|
</div>
|
||||||
|
{{block "new" .}}{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
40
web/budget-new.tpl
Normal file
40
web/budget-new.tpl
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{{define "budget-new"}}
|
||||||
|
<div id="newbudgetmodal" class="modal fade" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('#errorcreatingbudget').hide();
|
||||||
|
$('#newbudgetform').ajaxForm({
|
||||||
|
error: function() {
|
||||||
|
$('#errorcreatingbudget').show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">New Budget</h5>
|
||||||
|
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="newbudgetform" action="/api/v1/budget/new" method="POST">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" name="name" class="form-control" placeholder="Name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorcreatingbudget">
|
||||||
|
Error creating budget.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<input type="submit" class="btn btn-primary" value="Create" class="form-control" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
48
web/budget-sidebar.tpl
Normal file
48
web/budget-sidebar.tpl
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{{define "budget-sidebar"}}
|
||||||
|
<h1><a href="/dashboard">⌂</a> {{.Budget.Name}}</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/budget/{{.Budget.ID}}">Budget</a></li>
|
||||||
|
<li>Reports (Coming Soon)</li>
|
||||||
|
<li><a href="/budget/{{.Budget.ID}}/all-accounts">All Accounts</a></li>
|
||||||
|
<li>
|
||||||
|
On-Budget Accounts
|
||||||
|
<ul class="two-valued">
|
||||||
|
{{- range .OnBudgetAccounts}}
|
||||||
|
<li>
|
||||||
|
<a href="/budget/{{$.Budget.ID}}/account/{{.ID}}">{{.Name}}</a>
|
||||||
|
{{- template "amount" .Balance}}
|
||||||
|
</li>
|
||||||
|
{{- end}}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Off-Budget Accounts
|
||||||
|
<ul class="two-valued">
|
||||||
|
{{- range .OffBudgetAccounts}}
|
||||||
|
<li>
|
||||||
|
<a href="/budget/{{$.Budget.ID}}/account/{{.ID}}">{{.Name}}</a>
|
||||||
|
{{template "amount" .Balance -}}
|
||||||
|
</li>
|
||||||
|
{{- end}}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Closed Accounts
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/budget/{{$.Budget.ID}}/accounts">Edit accounts</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
+ Add Account
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/budget/{{.Budget.ID}}/settings">Budget-Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin">Admin</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/api/v1/user/logout">Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
50
web/budgeting.html
Normal file
50
web/budgeting.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}
|
||||||
|
{{printf "Budget for %s %d" .Date.Month .Date.Year}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "new"}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<div class="budget-item">
|
||||||
|
<a href="#newtransactionmodal" data-bs-toggle="modal" data-bs-target="#newtransactionmodal">New Transaction</a>
|
||||||
|
<span class="time"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{{printf "/budget/%s/%d/%d" .Budget.ID .Previous.Year .Previous.Month}}">Previous Month</a> -
|
||||||
|
<a href="{{printf "/budget/%s" .Budget.ID}}">Current Month</a> -
|
||||||
|
<a href="{{printf "/budget/%s/%d/%d" .Budget.ID .Next.Year .Next.Month}}">Next Month</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Available Balance: </span>{{template "amountf64" .AvailableBalance}}
|
||||||
|
</div>
|
||||||
|
<table class="container col-lg-12" id="content">
|
||||||
|
<tr>
|
||||||
|
<th>Group</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
<th>Leftover</th>
|
||||||
|
<th>Assigned</th>
|
||||||
|
<th>Activity</th>
|
||||||
|
<th>Available</th>
|
||||||
|
</tr>
|
||||||
|
{{range .Categories}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Group}}</td>
|
||||||
|
<td>{{.Name}}</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
{{template "amountf64-cell" .AvailableLastMonth}}
|
||||||
|
{{template "amountf64-cell" .Assigned}}
|
||||||
|
{{template "amountf64-cell" .Activity}}
|
||||||
|
{{template "amountf64-cell" .Available}}
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
{{end}}
|
26
web/dashboard.html
Normal file
26
web/dashboard.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{{define "title"}}
|
||||||
|
Budgets
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "new"}}
|
||||||
|
{{template "budget-new"}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "sidebar"}}
|
||||||
|
Please select a budget.
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
{{range .Budgets}}
|
||||||
|
<div class="budget-item">
|
||||||
|
<a href="budget/{{.ID}}">{{.Name}}</a>
|
||||||
|
<span class="time"></span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="budget-item">
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newbudgetmodal">New Budget</a>
|
||||||
|
<span class="time"></span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
17
web/index.html
Normal file
17
web/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{{define "title"}}
|
||||||
|
Start
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "new"}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<div class="container col-md-8 col-ld-8" id="content">
|
||||||
|
Willkommen bei Budgeteer, der neuen App für's Budget!
|
||||||
|
</div>
|
||||||
|
<div class="container col-md-4" id="login">
|
||||||
|
<a href="/login">Login</a> or <a href="/login">register</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
42
web/login.html
Normal file
42
web/login.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}Login{{end}}
|
||||||
|
|
||||||
|
{{define "more-head"}}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#invalidCredentials').hide();
|
||||||
|
$('#loginForm').ajaxForm({
|
||||||
|
success: function() {
|
||||||
|
window.location.href = "/dashboard";
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$('#invalidCredentials').show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<form id="loginForm" action="/api/v1/user/login" method="POST" class="center-block">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">User</label>
|
||||||
|
<input type="text" name="username" class="form-control" placeholder="User" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" class="form-control" placeholder="Password" />
|
||||||
|
<p id="invalidCredentials">
|
||||||
|
The entered credentials are invalid
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" value="Login" class="btn btn-default" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
New user? <a href="/register">Register</a> instead!
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
72
web/register.html
Normal file
72
web/register.html
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{{define "title"}}Register{{end}}
|
||||||
|
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "more-head"}}
|
||||||
|
<script>
|
||||||
|
function checkPasswordMatchUi() {
|
||||||
|
if(checkPasswordMatch())
|
||||||
|
$("#divCheckPasswordMatch").html("Passwords match.");
|
||||||
|
else
|
||||||
|
$("#divCheckPasswordMatch").html("Passwords do not match!");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPasswordMatch() {
|
||||||
|
var password = $("#password").val();
|
||||||
|
var confirmPassword = $("#password_confirm").val();
|
||||||
|
return password == confirmPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$("#password, #password_confirm").keyup(checkPasswordMatchUi);
|
||||||
|
$('#invalidCredentials').hide();
|
||||||
|
$('#loginForm').ajaxForm({
|
||||||
|
beforeSubmit: function(a, b, c) {
|
||||||
|
var match = checkPasswordMatch();
|
||||||
|
if(!match){
|
||||||
|
$("#divCheckPasswordMatch").fadeOut(300).fadeIn(300).fadeOut(300).fadeIn(300);
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
},
|
||||||
|
success: function() {
|
||||||
|
window.location.href = "/dashboard";
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$('#invalidCredentials').show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<form id="loginForm" action="/api/v1/user/register" method="POST" class="center-block">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
<input type="text" name="email" class="form-control" placeholder="E-Mail" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" name="name" class="form-control" placeholder="Name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" id="password" class="form-control" placeholder="Password" />
|
||||||
|
<input type="password" id="password_confirm" class="form-control" placeholder="Verify password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="divCheckPasswordMatch"></div>
|
||||||
|
|
||||||
|
<div id="invalidCredentials">
|
||||||
|
Username already exists
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" value="Login" class="form-control" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Existing user? <a href="/login">Login</a> instead!
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
32
web/settings.html
Normal file
32
web/settings.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{{define "title"}}
|
||||||
|
Settings for Budget "{{.Budget.Name}}"
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<h1>Danger Zone</h1>
|
||||||
|
<div class="budget-item">
|
||||||
|
<a href="/budget/{{.Budget.ID}}/settings/clear">Clear budget</a>
|
||||||
|
<p>This removes transactions and assignments to start from scratch. Accounts and categories are kept. Not undoable!</p>
|
||||||
|
</div>
|
||||||
|
<div class="budget-item">
|
||||||
|
<a href="/budget/{{.Budget.ID}}/settings/clean-negative">Fix all historic negative category-balances</a>
|
||||||
|
<p>This restores YNABs functionality, that would substract any overspent categories' balances from next months inflows.</p>
|
||||||
|
</div>
|
||||||
|
<div class="budget-item">
|
||||||
|
<form method="POST" action="/api/v1/transaction/import/ynab" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="budget_id" value="{{.Budget.ID}}" />
|
||||||
|
<label for="transactions_file">
|
||||||
|
Transaktionen:
|
||||||
|
<input type="file" name="transactions" accept="text/*" />
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label for="assignments_file">
|
||||||
|
Budget:
|
||||||
|
<input type="file" name="assignments" accept="text/*" />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Importieren</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
7
web/static/css/bootstrap.min.css
vendored
Normal file
7
web/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/static/css/bootstrap.min.css.map
Normal file
1
web/static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
77
web/static/css/main.css
Normal file
77
web/static/css/main.css
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#head {
|
||||||
|
height:160px;
|
||||||
|
line-height: 160px;
|
||||||
|
font-size:200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login */
|
||||||
|
#loginForm{
|
||||||
|
width:600px;
|
||||||
|
}
|
||||||
|
#invalidCredentials {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Budgets */
|
||||||
|
.budget-item {
|
||||||
|
width: 11.3em;
|
||||||
|
height: 11.3em;
|
||||||
|
border: 1px solid dimgray;
|
||||||
|
border-radius: 0.707em;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
.budget-item a {
|
||||||
|
margin: 8em auto 0.5em;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.budget-item .time {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 70.7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
margin: 1em;
|
||||||
|
font-size: 180%;
|
||||||
|
}
|
||||||
|
#sidebar ul {
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
font-size: 75%;
|
||||||
|
}
|
||||||
|
.two-valued {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
.two-valued * {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
.two-valued * * {
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlights */
|
||||||
|
.negative {
|
||||||
|
color: #d50000;
|
||||||
|
}
|
||||||
|
.zero {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
.future {
|
||||||
|
background-color: #cccccc;
|
||||||
|
}
|
Before Width: | Height: | Size: 766 B After Width: | Height: | Size: 766 B |
BIN
web/static/fonts/glyphicons-halflings-regular.eot
Normal file
BIN
web/static/fonts/glyphicons-halflings-regular.eot
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user