Compare commits
255
Commits
v0.0.1
...
79bbda884c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79bbda884c | ||
|
|
0583d69c4a | ||
|
|
2f75aae2a0 | ||
|
|
68a5153481 | ||
|
|
6dae0cfa4c | ||
|
|
f0c3caaa79 | ||
|
|
0184cbd9cd | ||
|
|
332f587bcf | ||
|
|
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 |
No files matched your search
@@ -0,0 +1,10 @@
|
|||||||
|
build/
|
||||||
|
.git/
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
Earthfile
|
||||||
|
config.example.json
|
||||||
|
.gitignore
|
||||||
|
.vscode/
|
||||||
|
budgeteer
|
||||||
|
budgeteer.exe
|
||||||
+27
@@ -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
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
build/
|
||||||
|
.git/
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
Earthfile
|
||||||
|
config.example.json
|
||||||
|
.gitignore
|
||||||
|
.vscode/
|
||||||
|
budgeteer
|
||||||
|
budgeteer.exe
|
||||||
+80
-19
@@ -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
|
||||||
Vendored
+24
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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": "."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM scratch
|
||||||
|
COPY ./budgeteer /app/budgeteer
|
||||||
|
ENTRYPOINT ["/app/budgeteer"]
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"DatabaseHost": "localhost",
|
||||||
|
"DatabaseUser": "user",
|
||||||
|
"DatabasePassword": "thisismypassword",
|
||||||
|
"DatabaseName": "budgeteer"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
// 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 AND transactions.date < NOW()
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- 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 AND transactions.date < NOW()
|
||||||
|
WHERE accounts.budget_id = $1
|
||||||
|
GROUP BY accounts.id, accounts.name
|
||||||
|
ORDER BY accounts.name;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- name: LinkBudgetToUser :one
|
||||||
|
INSERT INTO user_budgets
|
||||||
|
(user_id, budget_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING *;
|
||||||
@@ -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 *;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP EXTENSION "uuid-ossp";
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE transactions ADD COLUMN group_id uuid NULL;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE transactions DROP COLUMN group_id;
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
@@ -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}}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
@charset "UTF-8";/*!
|
||||||
|
* Bootstrap v5.1.3 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2021 The Bootstrap Authors
|
||||||
|
* Copyright 2011-2021 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
*/:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:iLine truncated
|
||||||
|
/*# sourceMappingURL=bootstrap.min.css.map */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"iBAAA;;;;;ACAA,MAQI,UAAA,QAAA,YAAA,QAAA,YAAA,QAAA,UAAA,QAAA,SAAA,QAAA,YAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAAA,UAAA,QAAA,WAAA,KAAA,UAAA,QAAA,eAAA,QAIA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAIA,aAAA,QAAA,eAAA,QAAA,aAAA,QAAA,UAAA,QAAA,aAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAIA,iBAAA,EAAA,CAAA,GAAA,CAAA,IAAA,mBAAA,GAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,EAAA,CAAA,GAAA,CAAA,GAAA,cAAA,EAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,GAAA,CAAA,GAAA,CAAA,EAAA,gBAAA,GAAA,CAAA,EAAA,CAAA,GAAA,eAAA,GAAA,CAAA,GAAA,CAAA,IAAA,cAAA,EAAA,CAAA,EAAA,CAAA,GAGF,eAAA,GAAA,CAAA,GAAA,CAAA,IACA,eAAA,CAAA,CAAA,CAAA,CAAA,EACA,oBAAA,EAAA,CAAA,EAAA,CAAA,GACA,iBAAA,GAAA,CAAA,GAAA,CAAA,IAMA,qBAAA,SAAA,CAAA,aAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,iBAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBACA,oBAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UACA,cAAA,2EAQA,sBAAA,0BACA,oBAAA,KACA,sBAAA,IACA,sBAAA,IACA,gBAAA,QAIA,aAAA,KCnCF,ECgDA,QADA,SD5CE,WAAA,WAeE,8CANJ,MAOM,gBAAA,QAcN,KACE,OAAA,EACA,YAAA,2BEmPI,UAAA,yBFjPJ,YAAA,2BACA,YAAA,2BACA,MAAA,qBACA,WAAA,0BACA,iBAAA,kBACA,yBAAA,KACA,4BAAA,YAUF,GACE,OAAA,KAAA,EACA,MAAA,QACA,iBAAA,aACA,OAAA,EACA,QAAA,IAGF,eACE,OAAA,IAUF,IAAA,IAAA,IAAA,IAAA,IAAA,IAAA,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAGA,YAAA,IACA,YAAA,IAIF,IAAA,GEwMQ,UAAA,uBAlKJ,0BFtCJ,IAAA,GE+MQ,UAAA,QF1MR,IAAA,GEmMQ,UAAA,sBAlKJ,0BFjCJ,IAAA,GE0MQ,UAAA,MFrMR,IAAA,GE8LQ,UAAA,oBAlKJ,0BF5BJ,IAAA,GEqMQ,UAAA,SFhMR,IAAA,GEyLQ,UAAA,sBAlKJ,0BFvBJ,IAAA,GEgMQ,UAAA,QF3LR,IAAA,GEgLM,UAAA,QF3KN,IAAA,GE2KM,UAAA,KFhKN,EACE,WAAA,EACA,cAAA,KCoBF,6BDTA,YAEE,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,iCAAA,KAAA,yBAAA,KAMF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QAMF,GCKA,GDHE,aAAA,KCSF,GDNA,GCKA,GDFE,WAAA,EACA,cAAA,KAGF,MCMA,MACA,MAFA,MDDE,cAAA,EAGF,GACE,YAAA,IAKF,GACE,cAAA,MACA,YAAA,EAMF,WACE,OAAA,EAAA,EAAA,KAQF,ECLA,ODOE,YAAA,OAQF,OAAA,ME4EM,UAAA,OFrEN,MAAA,KACE,QAAA,KACA,iBAAA,QASF,ICnBA,IDqBE,SAAA,SEwDI,UAAA,MFtDJ,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAKN,EACE,MAAA,QACA,gBAAA,UAEA,QACE,MAAA,QAWF,2BAAA,iCAEE,MAAA,QACA,gBAAA,KCvBJ,KACA,ID6BA,IC5BA,KDgCE,YAAA,yBEcI,UAAA,IFZJ,UAAA,IACA,aAAA,cAOF,IACE,QAAA,MACA,WAAA,EACA,cAAA,KACA,SAAA,KEAI,UAAA,OFKJ,SELI,UAAA,QFOF,MAAA,QACA,WAAA,OAIJ,KEZM,UAAA,OFcJ,MAAA,QACA,UAAA,WAGA,OACE,MAAA,QAIJ,IACE,QAAA,MAAA,MExBI,UAAA,OF0BJ,MAAA,KACA,iBAAA,QG7SE,cAAA,MHgTF,QACE,QAAA,EE/BE,UAAA,IFiCF,YAAA,IASJ,OACE,OAAA,EAAA,EAAA,KAMF,IChDA,IDkDE,eAAA,OAQF,MACE,aAAA,OACA,gBAAA,SAGF,QACE,YAAA,MACA,eAAA,MACA,MAAA,QACA,WAAA,KAOF,GAEE,WAAA,QACA,WAAA,qBCvDF,MAGA,GAFA,MAGA,GDsDA,MCxDA,GD8DE,aAAA,QACA,aAAA,MACA,aAAA,EAQF,MACE,QAAA,aAMF,OAEE,cAAA,EAQF,iCACE,QAAA,ECrEF,OD0EA,MCxEA,SADA,OAEA,SD4EELine truncated
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user