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