254 Commits

Author SHA1 Message Date
2843d8a2f1 Save all unmatched transfers as regular transactions
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2022-01-10 10:10:02 +00:00
843dcd2536 Fix logging wrong objects 2022-01-10 10:10:02 +00:00
a147830e12 Also fetch GroupID and highlight groups in transactions-view 2022-01-10 10:10:02 +00:00
b0776023b4 Fix migration 2022-01-10 10:10:02 +00:00
0b95cdc1d9 Reword clear actions description 2022-01-10 10:10:02 +00:00
2ec9c923df Implement matching 2022-01-10 10:10:02 +00:00
beff7afcf7 Add group_id 2022-01-10 10:10:02 +00:00
951e827d20 Add transfer_id 2022-01-09 20:47:43 +00:00
2f3e4bc748 Add transfers to list and skip 2022-01-09 20:27:51 +00:00
d71eb17092 Remove new transaction from transaction edit dialog
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:41:18 +00:00
53dd31fa35 Extract util.go 2021-12-28 20:41:06 +00:00
1a4267186a Add ability to edit payees 2021-12-28 20:40:53 +00:00
5018e5b973 Give sensible name to caching method
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:16:44 +00:00
ed9e75d57a Enable drone CI
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:08:19 +00:00
ed361324dd Fix indentation 2021-12-28 16:14:48 +00:00
6bac09a38e Implement update and delete for transactions 2021-12-27 23:38:30 +00:00
ab43387f06 Use wrapper to prevent password change popups from screwing up the layout 2021-12-27 23:14:50 +00:00
c112d95a41 Add delete button 2021-12-27 23:07:16 +00:00
6fdc0e3b1d Try to fix indentation 2021-12-27 23:07:07 +00:00
f08784ffa7 Fix typo 2021-12-27 23:06:59 +00:00
8188184ac9 Add tag for self-hosted registry 2021-12-27 23:06:38 +00:00
81b3bf334a Add transaction detail view 2021-12-14 15:02:51 +00:00
d0ad0dcb3a Implement categories for new transactions 2021-12-14 14:41:10 +00:00
1ab1fa74e0 Improve new-transaction form by adding account_id and default date 2021-12-14 14:15:08 +00:00
33c54c9f4c Make import available via UI 2021-12-14 14:14:35 +00:00
1ed9344586 Add home button to sidebar 2021-12-14 14:14:12 +00:00
a8bd03a805 Handle circular required keys
Use a dummy-value at first and update it later.
Deferrable doesn't seem to work for NOT NULL - only
for FOREIGN KEYs.
2021-12-14 14:13:23 +00:00
9e01be699a Fix modals not opening 2021-12-14 14:12:01 +00:00
84ddb36d62 Fix reset only undoing one version 2021-12-14 14:11:11 +00:00
8b6a8c3697 Add income_category_id only after categories table exists 2021-12-11 22:08:08 +00:00
208ffce968 Wrap errors 2021-12-11 22:07:53 +00:00
bfba5f4028 Also rebuild on schema change
Files in postgres/schema/ are embedded in an embed.FS
2021-12-11 22:07:34 +00:00
1f2d81f173 Add task build command 2021-12-11 22:07:09 +00:00
c3a93377d9 Fix schema 2021-12-11 21:55:33 +00:00
40a299141d Implement new budget with transaction to be able to satisfy not null columns 2021-12-11 20:19:52 +00:00
935499e3a8 Improve UI
Highlight future transactions
clarify settings are for budget
2021-12-11 20:19:28 +00:00
915964fa4e Add now to funcs available from templates 2021-12-11 20:18:27 +00:00
e9adc763b2 Remove Repository and use Database instead 2021-12-11 20:18:09 +00:00
d5ebf5a5cf Group hidden categories 2021-12-11 15:10:51 +00:00
466775817f Merge added and assigned into moneyUsed 2021-12-11 13:03:26 +00:00
e2413290b4 Extract another method 2021-12-11 13:01:35 +00:00
18cd29cca2 Exctract getDate 2021-12-11 12:52:52 +00:00
caf0126b86 Implement budgeting views by calculating most values locally 2021-12-11 12:47:41 +00:00
6da1b26a2f update go.mod 2021-12-11 12:47:07 +00:00
13993b6b5a Try to calculate balances locally 2021-12-10 18:56:56 +00:00
625e0635fd Fix indentation 2021-12-10 17:09:43 +00:00
1826274ccc Show budget name in sidebar 2021-12-10 16:40:02 +00:00
defbbd1884 Improve documentation for YNAB Import 2021-12-10 16:39:56 +00:00
8116238d48 Add settings page linking to clean and clear 2021-12-10 16:39:44 +00:00
e0eeaadc60 Use same template for account and all-accounts 2021-12-10 16:38:10 +00:00
4cd81592e4 Also watch for templates changes 2021-12-10 16:37:11 +00:00
5d9693838f Complete Taskfile
- Use checksums for go.mod/go.sum
- Disable CGO to be able to use 'FROM SCRATCH' Dockerfile
- Add run command that updates docker-compose
2021-12-10 09:43:59 +00:00
3bec0857d5 Add Taskfile 2021-12-10 09:36:16 +00:00
5e18d51b5d Only show last month's overflow 2021-12-08 15:21:10 +00:00
11179a1593 Also load available last month 2021-12-08 15:15:49 +00:00
7cb7527704 Use date_trunc instead of splitting date into year and month 2021-12-08 15:15:35 +00:00
c3a022b595 Add views with results grouped by month 2021-12-08 14:40:11 +00:00
a0ebdd01aa Implement cleaning to set all historic negative balances to zero 2021-12-07 21:59:06 +00:00
edd1319222 Show available balance including activities of this month 2021-12-07 21:32:36 +00:00
a19d3d6932 Try to enable caching 2021-12-07 21:32:20 +00:00
f4ddf12214 Split displayed accounts by on- or off-budget 2021-12-07 21:20:35 +00:00
04fd687324 Handle null values in numeric 2021-12-07 21:20:35 +00:00
cbda69e827 Handle on_budget in available balance 2021-12-07 21:20:35 +00:00
e3f3dc6748 Add on_budget column to accounts 2021-12-07 21:20:35 +00:00
915379f5cb Make available balance date-dependent 2021-12-07 21:20:35 +00:00
284685fb52 Update Bootstrap 2021-12-07 21:20:35 +00:00
5f4c5d9d51 Display zero values in grey 2021-12-07 20:35:49 +00:00
8c9c78a789 Fix between call being inclusive 2021-12-07 20:28:48 +00:00
64822912d9 Add available balance 2021-12-07 20:22:40 +00:00
1d4bc158a8 Improve handling of context 2021-12-07 19:08:53 +00:00
fbd283cd1c Show date in without time 2021-12-07 15:42:39 +00:00
0ee3f269b5 Split routes into own files 2021-12-07 15:42:29 +00:00
6bcf94661e Merge balance and balanceassignments 2021-12-07 15:33:08 +00:00
0b0b20c5ec Implement assignment import and clear of budget 2021-12-07 15:28:40 +00:00
6f4bff929e Extract amount template 2021-12-07 14:49:58 +00:00
4a0759af8f Sort categories and add link to current month 2021-12-07 13:45:21 +00:00
646560267a Add ability to select month for budget 2021-12-07 13:40:24 +00:00
53c51ceb8d Add budgeting 2021-12-07 13:32:29 +00:00
8f374f1d62 Try to display simple budget screen 2021-12-07 13:02:39 +00:00
494c1431fe Hide category if empty 2021-12-06 21:21:44 +00:00
82045ceed7 Fix joins 2021-12-06 21:18:29 +00:00
16b59afc29 Abort if invalid token/data and wrap some errors 2021-12-06 21:12:32 +00:00
29cee46a14 Display accounts sorted by name 2021-12-06 20:47:47 +00:00
2caaa1a048 Fix creating group with name of category 2021-12-06 20:46:25 +00:00
e3cf69ab08 Remember last login 2021-12-06 20:38:55 +00:00
1437fc7b8d Fix 0001_enable-uuid-ossp and reorder 2021-12-06 20:36:10 +00:00
e2d22a1080 Display category in accounts view 2021-12-06 20:26:06 +00:00
495bc2b7c3 Import category 2021-12-06 20:23:10 +00:00
674028d394 Link to accounts view from sidebar 2021-12-04 23:57:41 +00:00
f103cfe3e3 Disable accounts-sidebar for admin 2021-12-04 23:57:18 +00:00
21db0fb3aa Fix link to account 2021-12-04 23:57:09 +00:00
779733e0ab Admin routes don't need budget 2021-12-04 23:56:58 +00:00
2ec3873e83 Revert order of template and actual page 2021-12-04 23:46:13 +00:00
91a0c43d6d Speed up build by using cache and updating ignores 2021-12-04 23:45:58 +00:00
69bac9bc3f Reorganize routes 2021-12-04 23:12:26 +00:00
95f6e95fdd Add link to admin-panel 2021-12-04 23:12:12 +00:00
5321c7d85f Improve sidebar template 2021-12-04 23:03:49 +00:00
ba0926900a Move account view below budget 2021-12-04 22:59:12 +00:00
61fa6ed776 Extract getImportantData to own file 2021-12-04 22:58:52 +00:00
6628a4849f Extract loading of accounts to middleware 2021-12-04 22:47:01 +00:00
e30fab6a06 Add sidebar 2021-12-04 22:40:27 +00:00
cb1297b8bf Fix using model directly 2021-12-02 21:51:36 +00:00
223064698e Fix type overrides
See https://github.com/kyleconroy/sqlc/issues/819
2021-12-02 21:50:46 +00:00
c56335adad Finalize account view 2021-12-02 21:49:14 +00:00
871b11bbcc Implement single account transaction-list 2021-12-02 21:44:35 +00:00
f2e8721aa8 Fix not loading any accounts and payees 2021-12-02 21:41:55 +00:00
4646356b2d Introduce own Numeric type and revert to stdlib from pgx 2021-12-02 21:32:01 +00:00
6f8a94ff5d Show Account Balance 2021-12-02 21:29:11 +00:00
276fdb4ade Also display account & payee 2021-12-02 20:58:27 +00:00
a4659c7133 Use printf in template 2021-12-02 20:53:05 +00:00
696469f6c2 Handle amount in import and display 2021-12-02 20:49:07 +00:00
db7f0bfb7b Add login/register link from register/login 2021-12-02 20:37:40 +00:00
0855942d0d Fix order of Down query 2021-12-02 20:37:16 +00:00
dcdcd53833 Fix users ID not being generated 2021-12-02 20:36:55 +00:00
11d20eeb66 Add admin routes 2021-12-02 20:36:40 +00:00
4011f3cace Also return sql.DB to be able to use goose 2021-12-02 20:36:22 +00:00
a3df95a700 Move session methods to own file 2021-12-02 20:09:44 +00:00
36bccce021 Remove token from data and add to gin context 2021-12-02 20:06:26 +00:00
e7c9a7f52f Rename budget to transaction for new-transaction 2021-12-02 18:45:03 +00:00
67139ceff8 Show first 200 transactions only 2021-12-02 16:23:59 +00:00
61c55dda3a Small fixes for ynab import 2021-12-02 16:22:27 +00:00
e9173a3d37 Pass budgetID 2021-12-02 15:58:18 +00:00
57542b5202 Skip first line 2021-12-02 15:58:10 +00:00
1cd2eedcb8 Continue on ynab import 2021-12-02 15:56:40 +00:00
7b6914e5f2 Start implementing ynab import 2021-12-02 15:30:05 +00:00
63c1b4fbab Use /app in production image 2021-12-02 15:28:45 +00:00
77ae9d2dfd Convert templates to partials 2021-12-02 15:00:53 +00:00
67f7022b90 Fix included libraries 2021-12-02 10:58:10 +00:00
f760f9b855 Add accounts 2021-12-02 10:52:58 +00:00
e465b961a5 Use pgx 2021-12-02 10:44:14 +00:00
cdc767a497 Regenerate sqlc 2021-12-02 10:44:01 +00:00
adce350e0c Try to implement new transaction 2021-12-02 10:43:42 +00:00
4d1b883974 Merge schemas into one file 2021-12-02 10:09:57 +00:00
37d19733df Add accounts & payees 2021-11-29 22:52:58 +00:00
6df72dc40d Split budget and dashboard routes into own files 2021-11-29 22:29:03 +00:00
f0a1d1d475 ignore .vscode for build 2021-11-29 22:28:41 +00:00
01e634333a Improve .gitignore 2021-11-29 22:08:33 +00:00
8e8c653fc9 Add run target 2021-11-29 22:04:04 +00:00
85ef7557c1 Use uuid.UUID everywhere and have postgres generate ids 2021-11-29 21:49:37 +00:00
5e8a98872f Add adminer container 2021-11-29 21:41:57 +00:00
8aa147f2a5 Rebuild go.mod 2021-11-08 22:38:11 +00:00
45d01b4e84 Fix templates 2021-11-08 22:36:31 +00:00
c4fe36a2a4 Fix leading space 2021-11-08 22:33:11 +00:00
bda3d32cfc Use postgres schema instead of pgx 2021-11-08 22:29:35 +00:00
f40a05f92d Use log instead of panic 2021-11-08 22:29:26 +00:00
19fef9cc05 Cleanup go.mod 2021-11-08 22:24:58 +00:00
14df8ff84b Add things to earthignore 2021-11-08 22:24:31 +00:00
5de7d32c30 Use goose for migrations 2021-11-08 22:24:21 +00:00
cf1bc70103 Begin migration to sqlc 2021-11-08 21:32:30 +00:00
f019c47d21 Remove tmp 2021-11-07 22:06:42 +00:00
bc315961af Ignore .git 2021-11-07 22:06:15 +00:00
736343a3dd Add docker-compose.yml 2021-11-07 22:04:31 +00:00
30b306e485 Fix subdir static/ 2021-11-07 22:04:22 +00:00
a891047b7a Add earthignore 2021-11-07 22:03:35 +00:00
4b08de00b8 Use embedded files 2021-11-07 22:01:13 +00:00
f33d4b6cae Update go.mod 2021-11-07 22:01:05 +00:00
4658b40bd9 Move web files to subdir 2021-11-07 22:00:53 +00:00
9bc0f2054e Fix Earthfile 2021-11-07 22:09:26 +01:00
5128ac4c3b Add first Earthfile 2021-11-07 21:57:27 +01:00
06f7a57565 Add go.sum 2021-11-07 21:57:16 +01:00
9b04ea8dc6 Update gin 2021-11-07 21:56:42 +01:00
2eb1457019 Load config from ENV 2021-11-07 21:56:35 +01:00
5518d57bbd Init go.mod 2021-11-07 20:57:33 +01:00
caa10ee560 Add Budget API route 2017-02-04 21:42:31 +01:00
39bd56a30f Add href to link to fix cursor 2017-01-19 23:10:42 +01:00
2e8f21ea8c Fix display and link of Budget 2017-01-19 23:10:21 +01:00
b76f720156 Add route for NewBudget 2017-01-19 23:10:04 +01:00
ddbda095b5 Implement NewBudget in BudgetService 2017-01-19 23:09:56 +01:00
ddf66dd8b0 Add bootstrap js 2017-01-19 22:55:08 +01:00
c598694eb4 Improve modal 2017-01-19 22:55:02 +01:00
019ffa34cd Add modal for new budget 2017-01-19 22:37:55 +01:00
725555bdc9 Remove sublime project 2017-01-19 22:36:30 +01:00
a504e4d382 Move login, logout and register to /api/v1/user 2017-01-19 22:17:53 +01:00
2619566a62 Remove budgets list 2016-12-31 03:45:52 +01:00
9e7701cf06 Style Budgets screen 2016-12-31 03:45:43 +01:00
4bdc292570 Fix title 2016-12-31 03:42:12 +01:00
9a399c13a3 Remove login test-routes 2016-12-31 03:28:29 +01:00
917251a16e Forward to dashboard when logged in 2016-12-31 03:27:52 +01:00
073b17b4c0 Add ModelService 2016-12-27 00:47:56 +01:00
e8574ebd0a Use TemplateData for dashboard 2016-12-27 00:47:47 +01:00
1ee88466f2 Use Token field instead of GetToken 2016-12-27 00:47:36 +01:00
e525fdc928 Rename to Service
Add ModelService interface to merge all Services
2016-12-27 00:47:10 +01:00
5b9b2e02a0 Return pointers 2016-12-27 00:45:30 +01:00
30d9da1c81 Show budgets on dashboard 2016-12-27 00:29:01 +01:00
5754b97e4d Add data 2016-12-27 00:28:52 +01:00
0037cf045c Fix Budgets relation 2016-12-27 00:28:31 +01:00
649b272caf Use AbortWithError 2016-12-27 00:28:19 +01:00
7c8698da86 Add ID to token 2016-12-27 00:28:05 +01:00
f36fe1b1c0 Use id to load budgets 2016-12-27 00:27:52 +01:00
72fbec1063 Implement BudgetsForUser 2016-12-25 20:20:58 +01:00
57a69c448d Add userbudget 2016-12-20 21:17:17 +01:00
07d5816251 Implement budgetservice.Budget(id) 2016-12-20 20:52:48 +01:00
672376a55c Implement all services in type Repository 2016-12-20 20:46:25 +01:00
04af4bce7e Add budget 2016-12-20 20:20:52 +01:00
ca1c2ed778 Add dashboard 2016-12-20 20:05:15 +01:00
135b1b8e8d Use form-groups in register as well 2016-12-20 19:57:20 +01:00
9a206250e8 Add placeholders 2016-12-20 19:44:56 +01:00
aba366f211 Use form groups 2016-12-20 19:44:56 +01:00
2b2f508998 Remove static index.html 2016-12-20 19:23:18 +01:00
7c197ff49d Use string instead of ID type 2016-12-20 16:52:26 +01:00
83c36f00bf Use whole array as hash 2016-12-20 16:40:33 +01:00
6a8818dab6 Cast to budgeteer.ID 2016-12-20 16:39:09 +01:00
97ca85a0db Field is called email not username 2016-12-20 16:38:48 +01:00
518a0c2df9 Create ulid generator 2016-12-20 16:21:12 +01:00
4b7a70d03d Fix width with passwords-match-notice 2016-12-20 16:20:29 +01:00
a71afaf6b9 Implement registration 2016-12-20 15:26:31 +01:00
b9d428d386 Implement bcrypt 2016-12-20 15:15:33 +01:00
cf03726643 Use spaces 2016-12-20 14:52:01 +01:00
88eb29f855 Use span instead of div 2016-12-20 14:49:48 +01:00
3406ce58cf Flash notice on submit if passwords don't match 2016-12-20 14:34:29 +01:00
9db0ea1870 Extract js Method checkPasswordMatch 2016-12-20 14:34:00 +01:00
6b730d5b33 Check if passwords match with js 2016-12-20 13:55:05 +01:00
2c55fbc431 Add db-init script for users-table 2016-12-20 13:43:45 +01:00
b5114beacf Accept user in CreateToken 2016-12-20 13:43:15 +01:00
e955638510 Actually compare to users in database 2016-12-20 13:36:30 +01:00
7b235f83ad Add registration 2016-12-20 13:34:34 +01:00
cb6558a8ce Implement CreateUser & use ID 2016-12-20 11:45:34 +01:00
f66e544d43 Add custom ID types 2016-12-20 11:42:57 +01:00
dafc477fe8 Add package ulid 2016-12-20 11:40:12 +01:00
2de8e46bf3 Read config and connect to db 2016-12-20 10:13:18 +01:00
528f329657 Add example config 2016-12-20 10:13:04 +01:00
65322bc182 Add config 2016-12-20 10:08:24 +01:00
c3664ef3e0 Actually use pg 2016-12-20 09:46:52 +01:00
0f1bd4cac3 Reformat http 2016-12-20 09:46:39 +01:00
308ff98830 Finish cleaning dependencies 2016-12-19 23:23:52 +01:00
02d6f3bd79 Add token.go 2016-12-19 22:48:36 +01:00
1592e4e8cf Move login.go to jwt subpackage 2016-12-19 22:48:17 +01:00
8231b3d176 Move login-code to http 2016-12-19 22:47:45 +01:00
227028f99d Try to extract http 2016-12-19 18:56:24 +01:00
0cac7a69aa Fix directory name 'postgres' 2016-12-19 18:56:09 +01:00
099ae5fe8a Add name to User 2016-12-19 18:46:35 +01:00
c0d6ae1157 Add postgres package 2016-12-19 18:43:24 +01:00
f759600db1 Move login & main to cmd/budgeteer 2016-12-19 18:43:07 +01:00
28b26c088e Fix login without fixed size 2016-12-07 17:19:38 +01:00
f4e227957b Add index 2016-12-07 17:19:11 +01:00
39141ab2e6 Rename template to login 2016-12-07 17:02:40 +01:00
f74b7ab078 Remove .html 2016-12-07 16:59:04 +01:00
ed0b939a75 Extract head to own template 2016-12-07 16:43:46 +01:00
1fa6c43df1 Extract css to main.css 2016-12-03 21:13:03 +01:00
147219e66d Center form and improve header 2016-12-03 21:11:45 +01:00
7a5bc35cc3 Add Header 2016-12-03 21:09:33 +01:00
e4755bcad8 Add DOCTYPE & viewport 2016-12-03 21:07:59 +01:00
36b066c687 Implement logout 2016-12-03 20:30:40 +01:00
304b954b51 Ignore swap files 2016-12-03 20:16:32 +01:00
bb6fd11a92 Include bootstrap css 2016-12-03 20:15:22 +01:00
637efda810 Actuall serve static files 2016-12-03 20:14:46 +01:00
825bf7be04 Add bootstrap 2016-12-03 20:13:47 +01:00
0adbb9087e Remove c&p comments 2016-11-25 18:55:11 +01:00
c8f221b310 Add .vscode to gitignore 2016-11-25 17:15:24 +01:00
3f8b88ecd4 Make invalid credentials notice red 2016-11-25 17:13:43 +01:00
ba7cea9965 Remove target param from form-action 2016-11-25 17:11:12 +01:00
db18731e43 Enable static files 2016-11-25 17:11:02 +01:00
a122e5db0c Use AJAX for login 2016-11-25 17:06:10 +01:00
b2ed65788e Get Name from Claims 2016-11-24 12:08:52 +01:00
118 changed files with 4955 additions and 644 deletions

10
.dockerignore Normal file
View 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
View 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
View File

@ -0,0 +1,10 @@
build/
.git/
docker-compose.yml
README.md
Earthfile
config.example.json
.gitignore
.vscode/
budgeteer
budgeteer.exe

99
.gitignore vendored
View File

@ -1,27 +1,88 @@
# ---> Go # From https://stackoverflow.com/questions/5711120/gitignore-binary-files-that-have-no-extension
# Compiled Object files, Static and Dynamic libs (Shared Objects) # Ignore all
*.o *
*.a
*.so
# Folders # Unignore all with extensions
_obj !*.*
_test
# Architecture specific extensions/prefixes # Unignore all dirs
*.[568vq] !*/
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,sublimetext,go
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,sublimetext,go
### Go ###
# Binaries for programs and plugins
*.exe *.exe
*.test *.exe~
*.prof *.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
### Go Patch ###
/vendor/
/Godeps/
### SublimeText ###
# Cache files for Sublime Text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# Workspace files are user-specific
*.sublime-workspace *.sublime-workspace
# Project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using Sublime Text
# *.sublime-project
# SFTP configuration file
sftp-config.json
sftp-config-alt*.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# Support for Project snippet scope
!.vscode/*.code-snippets
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,sublimetext,go

24
.vscode/tasks.json vendored Normal file
View 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
View 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
View 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
View 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
View 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
}

View File

@ -1,8 +0,0 @@
{
"folders":
[
{
"path": "."
}
]
}

3
build/Dockerfile Normal file
View File

@ -0,0 +1,3 @@
FROM scratch
COPY ./budgeteer /app/budgeteer
ENTRYPOINT ["/app/budgeteer"]

34
cmd/budgeteer/main.go Normal file
View 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
View File

@ -0,0 +1,6 @@
{
"DatabaseHost": "localhost",
"DatabaseUser": "user",
"DatabasePassword": "thisismypassword",
"DatabaseName": "budgeteer"
}

29
config/config.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}*/
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View File

@ -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
View File

@ -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
View 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
}

View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
}

View 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;

View 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;

View 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;

View 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;

View 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);

View 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;

View 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;

View File

@ -0,0 +1,5 @@
-- name: LinkBudgetToUser :one
INSERT INTO user_budgets
(user_id, budget_id)
VALUES ($1, $2)
RETURNING *;

View 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 *;

View File

@ -0,0 +1,5 @@
-- +goose Up
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- +goose Down
DROP EXTENSION "uuid-ossp";

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
}

View 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
View 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
View 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
View 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

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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">&times;</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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,32 @@
{{define "title"}}
Settings for Budget "{{.Budget.Name}}"
{{end}}
{{template "base" .}}
{{define "main"}}
<h1>Danger Zone</h1>
<div class="budget-item">
<a href="/budget/{{.Budget.ID}}/settings/clear">Clear budget</a>
<p>This removes transactions and assignments to start from scratch. Accounts and categories are kept. Not undoable!</p>
</div>
<div class="budget-item">
<a href="/budget/{{.Budget.ID}}/settings/clean-negative">Fix all historic negative category-balances</a>
<p>This restores YNABs functionality, that would substract any overspent categories' balances from next months inflows.</p>
</div>
<div class="budget-item">
<form method="POST" action="/api/v1/transaction/import/ynab" enctype="multipart/form-data">
<input type="hidden" name="budget_id" value="{{.Budget.ID}}" />
<label for="transactions_file">
Transaktionen:
<input type="file" name="transactions" accept="text/*" />
</label>
<br />
<label for="assignments_file">
Budget:
<input type="file" name="assignments" accept="text/*" />
</label>
<button type="submit">Importieren</button>
</form>
</div>
{{end}}

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

7
web/static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

77
web/static/css/main.css Normal file
View 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;
}

View File

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More