From 18149eef8b222ffdfb0074c92066d4fb2a7770b6 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 1 Mar 2022 08:33:01 +0000 Subject: [PATCH 01/14] Hide Leftover on small screens --- web/src/pages/Budgeting.vue | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/web/src/pages/Budgeting.vue b/web/src/pages/Budgeting.vue index 3ffb2c1..b1d05ac 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -82,20 +82,16 @@ function getGroupState(group : {Name : string, Expand: boolean}) : boolean { - - - + - {{ (getGroupState(group) ? "−" : "+") + " " + group.Name }} + - - -
CategoryLeftover Assigned Activity Available
{{ (getGroupState(group) ? "−" : "+") + " " + group.Name }}
{{ category.Name }} + From 6dd8a3791fa1c3426fec294fc3bafcf3238849b2 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 1 Mar 2022 08:36:03 +0000 Subject: [PATCH 02/14] Use grid instead of table --- web/src/pages/Budgeting.vue | 69 +++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/web/src/pages/Budgeting.vue b/web/src/pages/Budgeting.vue index b1d05ac..1c5332c 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -17,7 +17,7 @@ const CurrentBudgetID = computed(() => budgetsStore.CurrentBudgetID); const accountStore = useAccountStore(); const categoriesForMonth = accountStore.CategoriesForMonthAndGroup; -function GetCategories(group : string) { +function GetCategories(group: string) { return [...categoriesForMonth(selected.value.Year, selected.value.Month, group)]; }; @@ -28,20 +28,20 @@ const GroupsForMonth = computed(() => { const previous = computed(() => ({ - Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(), - Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(), + Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(), + Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(), })); const current = computed(() => ({ - Year: new Date().getFullYear(), - Month: new Date().getMonth(), + Year: new Date().getFullYear(), + Month: new Date().getMonth(), })); const selected = computed(() => ({ - Year: Number(props.year) ?? current.value.Year, - Month: Number(props.month ?? current.value.Month) + Year: Number(props.year) ?? current.value.Year, + Month: Number(props.month ?? current.value.Month) })); const next = computed(() => ({ - Year: new Date(selected.value.Year, Number(props.month) + 1, 1).getFullYear(), - Month: new Date(selected.value.Year, Number(props.month) + 1, 1).getMonth(), + Year: new Date(selected.value.Year, Number(props.month) + 1, 1).getFullYear(), + Month: new Date(selected.value.Year, Number(props.month) + 1, 1).getMonth(), })); watchEffect(() => { @@ -56,12 +56,12 @@ onMounted(() => { const expandedGroups = ref>(new Map()) -function toggleGroup(group : {Name : string, Expand: boolean}) { +function toggleGroup(group: { Name: string, Expand: boolean }) { console.log(expandedGroups.value); expandedGroups.value.set(group.Name, !(expandedGroups.value.get(group.Name) ?? group.Expand)) } -function getGroupState(group : {Name : string, Expand: boolean}) : boolean { +function getGroupState(group: { Name: string, Expand: boolean }): boolean { return expandedGroups.value.get(group.Name) ?? group.Expand; } @@ -79,31 +79,24 @@ function getGroupState(group : {Name : string, Expand: boolean}) : boolean { :to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month" >Next Month - - - - - - - - - - - - - - - - - - -
CategoryAssignedActivityAvailable
{{ (getGroupState(group) ? "−" : "+") + " " + group.Name }}
{{ category.Name }} - - - - - -
+
+ Category + + Assigned + Activity + Available + +
From dae618585726ecc98e9983a6aa1a1de2e4d24b0a Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 1 Mar 2022 18:33:46 +0000 Subject: [PATCH 03/14] Prevent startup on empty secret --- cmd/budgeteer/main.go | 11 +++++++---- jwt/login.go | 24 ++++++++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/cmd/budgeteer/main.go b/cmd/budgeteer/main.go index af132b3..208d455 100644 --- a/cmd/budgeteer/main.go +++ b/cmd/budgeteer/main.go @@ -29,11 +29,14 @@ func main() { panic("couldn't open static files") } + tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret) + if err != nil { + panic("couldn't create token verifier") + } + handler := &server.Handler{ - Service: queries, - TokenVerifier: &jwt.TokenVerifier{ - Secret: cfg.SessionSecret, - }, + Service: queries, + TokenVerifier: tokenVerifier, CredentialsVerifier: &bcrypt.Verifier{}, StaticFS: http.FS(static), } diff --git a/jwt/login.go b/jwt/login.go index bbb44c8..d58baef 100644 --- a/jwt/login.go +++ b/jwt/login.go @@ -11,8 +11,20 @@ import ( ) // TokenVerifier verifies Tokens. -type TokenVerifier struct { - Secret string +type tokenVerifier struct { + secret string +} + +var ErrEmptySecret = fmt.Errorf("secret is required") + +func NewTokenVerifier(secret string) (*tokenVerifier, error) { + if secret == "" { + return nil, ErrEmptySecret + } + + return &tokenVerifier{ + secret: secret, + }, nil } // Token contains everything to authenticate a user. @@ -28,7 +40,7 @@ const ( ) // CreateToken creates a new token from username and name. -func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) { +func (tv *tokenVerifier) CreateToken(user *postgres.User) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "usr": user.Email, "name": user.Name, @@ -37,7 +49,7 @@ func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) { }) // Generate encoded token and send it as response. - t, err := token.SignedString([]byte(tv.Secret)) + t, err := token.SignedString([]byte(tv.secret)) if err != nil { return "", fmt.Errorf("create token: %w", err) } @@ -52,12 +64,12 @@ var ( ) // VerifyToken verifys a given string-token. -func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn +func (tv *tokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod) } - return []byte(tv.Secret), nil + return []byte(tv.secret), nil }) if err != nil { return nil, fmt.Errorf("parse jwt: %w", err) From 09a227d08d859e071eb1bd83f0d03a61881a3287 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 1 Mar 2022 18:34:22 +0000 Subject: [PATCH 04/14] Wrap error for more details --- cmd/budgeteer/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/budgeteer/main.go b/cmd/budgeteer/main.go index 208d455..71b6eb1 100644 --- a/cmd/budgeteer/main.go +++ b/cmd/budgeteer/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io/fs" "log" "net/http" @@ -31,7 +32,7 @@ func main() { tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret) if err != nil { - panic("couldn't create token verifier") + panic(fmt.Errorf("couldn't create token verifier: %w", err)) } handler := &server.Handler{ From 3696bbde43f9762c55dc936e894901e5ce955876 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 1 Mar 2022 18:37:07 +0000 Subject: [PATCH 05/14] Check empty secret in other spots --- jwt/login.go | 18 +++++++++++++----- server/account_test.go | 7 +++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/jwt/login.go b/jwt/login.go index d58baef..190ebd7 100644 --- a/jwt/login.go +++ b/jwt/login.go @@ -11,18 +11,18 @@ import ( ) // TokenVerifier verifies Tokens. -type tokenVerifier struct { +type TokenVerifier struct { secret string } var ErrEmptySecret = fmt.Errorf("secret is required") -func NewTokenVerifier(secret string) (*tokenVerifier, error) { +func NewTokenVerifier(secret string) (*TokenVerifier, error) { if secret == "" { return nil, ErrEmptySecret } - return &tokenVerifier{ + return &TokenVerifier{ secret: secret, }, nil } @@ -40,7 +40,11 @@ const ( ) // CreateToken creates a new token from username and name. -func (tv *tokenVerifier) CreateToken(user *postgres.User) (string, error) { +func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) { + if tv.secret == "" { + return "", ErrEmptySecret + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "usr": user.Email, "name": user.Name, @@ -64,7 +68,11 @@ var ( ) // VerifyToken verifys a given string-token. -func (tv *tokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn +func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn + if tv.secret == "" { + return nil, ErrEmptySecret + } + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod) diff --git a/server/account_test.go b/server/account_test.go index 1af33c1..061b1bd 100644 --- a/server/account_test.go +++ b/server/account_test.go @@ -28,11 +28,10 @@ func TestRegisterUser(t *testing.T) { //nolint:funlen return } + tokenVerifier, _ := jwt.NewTokenVerifier("this_is_my_demo_secret_for_unit_tests") h := Handler{ - Service: database, - TokenVerifier: &jwt.TokenVerifier{ - Secret: "this_is_my_demo_secret_for_unit_tests", - }, + Service: database, + TokenVerifier: tokenVerifier, CredentialsVerifier: &bcrypt.Verifier{}, } From ecbb85aeaa7eb08ccbc7c0ccea6dbb1094594495 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 1 Mar 2022 18:52:04 +0000 Subject: [PATCH 06/14] Decrease displayed information --- web/src/pages/Budgeting.vue | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/web/src/pages/Budgeting.vue b/web/src/pages/Budgeting.vue index 1c5332c..0203b8a 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -71,30 +71,30 @@ function getGroupState(group: { Name: string, Expand: boolean }): boolean {
Previous Month- + ><<  Current Month- + >Current Month  Next Month + >>>
-
- Category - - Assigned - Activity - Available +
+ + + + + From 7c08ddacb7d22b906a75ad331d7ca48bfef9155b Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 1 Mar 2022 20:11:41 +0000 Subject: [PATCH 07/14] Remove widths from TransactionInputRow and use Button --- web/src/components/TransactionInputRow.vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/components/TransactionInputRow.vue b/web/src/components/TransactionInputRow.vue index 9eb3da8..1a1d8e8 100644 --- a/web/src/components/TransactionInputRow.vue +++ b/web/src/components/TransactionInputRow.vue @@ -3,6 +3,7 @@ import { computed, ref } from "vue"; import Autocomplete from '../components/Autocomplete.vue' import { Transaction, useTransactionsStore } from "../stores/transactions"; import DateInput from "./DateInput.vue"; +import Button from "./Button.vue"; const props = defineProps<{ budgetid: string @@ -51,28 +52,27 @@ function saveTransaction(e: MouseEvent) { \ No newline at end of file From 67c9b53e91abab5e0b048f95043449dce8cdce28 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 1 Mar 2022 20:11:59 +0000 Subject: [PATCH 08/14] Extract date to own row on small screens --- web/src/components/TransactionRow.vue | 39 ++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/web/src/components/TransactionRow.vue b/web/src/components/TransactionRow.vue index b182273..d5f2fff 100644 --- a/web/src/components/TransactionRow.vue +++ b/web/src/components/TransactionRow.vue @@ -5,6 +5,7 @@ import { useTransactionsStore } from "../stores/transactions"; import Currency from "./Currency.vue"; import TransactionEditRow from "./TransactionEditRow.vue"; import { formatDate } from "../date"; +import { useAccountStore } from "../stores/budget-account"; const props = defineProps<{ transactionid: string, @@ -18,17 +19,43 @@ const Reconciling = computed(() => useTransactionsStore().Reconciling); const transactionsStore = useTransactionsStore(); const TX = transactionsStore.Transactions.get(props.transactionid)!; + +function dateChanged() { + const currentAccount = useAccountStore().CurrentAccount; + if (currentAccount == null) + return true; + const transactionIndex = currentAccount.Transactions.indexOf(props.transactionid); + if(transactionIndex<=0) + return true; + + const previousTransactionId = currentAccount.Transactions[transactionIndex-1]; + const previousTransaction = transactionsStore.Transactions.get(previousTransactionId); + return TX.Date.getTime() != previousTransaction?.Date.getTime(); +} + +function getStatusSymbol() { + if(TX.Status == "Reconciled") + return "✔"; + + if(TX.Status == "Uncleared") + return "*"; + + return "✘"; +}