diff --git a/handler/middleware/cross_site_request_forgery.go b/handler/middleware/cross_site_request_forgery.go index 74710f2..90b5981 100644 --- a/handler/middleware/cross_site_request_forgery.go +++ b/handler/middleware/cross_site_request_forgery.go @@ -1,7 +1,6 @@ package middleware import ( - "fmt" "net/http" "strings" @@ -29,8 +28,6 @@ func (rr *csrfResponseWriter) Write(data []byte) (int, error) { dataStr := string(data) csrfToken, err := rr.auth.GetCsrfToken(rr.session) if err == nil { - csrfInput := fmt.Sprintf(``, csrfToken) - dataStr = strings.ReplaceAll(dataStr, "", csrfInput+"") dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", csrfToken) } @@ -48,10 +45,8 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler r.Method == http.MethodDelete || r.Method == http.MethodPatch { - csrfToken := r.FormValue("csrf-token") - if csrfToken == "" { - csrfToken = r.Header.Get("csrf-token") - } + csrfToken := r.Header.Get("csrf-token") + if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) { log.Info("CSRF-Token \"%s\" not correct", csrfToken) if r.Header.Get("HX-Request") == "true" { diff --git a/handler/transaction.go b/handler/transaction.go index 5713be6..78c5a46 100644 --- a/handler/transaction.go +++ b/handler/transaction.go @@ -50,7 +50,12 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc { return } - transactions, err := h.s.GetAll(user) + filter := types.TransactionItemsFilter{ + AccountId: r.URL.Query().Get("account-id"), + TreasureChestId: r.URL.Query().Get("treasure-chest-id"), + } + + transactions, err := h.s.GetAll(user, filter) if err != nil { handleError(w, r, err) return @@ -70,8 +75,13 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc { accountMap, treasureChestMap := h.getTransactionData(accounts, treasureChests) - comp := t.Transaction(transactions, accountMap, treasureChestMap) - h.r.RenderLayout(r, w, comp, user) + items := t.TransactionItems(transactions, accountMap, treasureChestMap) + if utils.IsHtmx(r) { + h.r.Render(r, w, items) + } else { + comp := t.Transaction(items, filter, accounts, treasureChests) + h.r.RenderLayout(r, w, comp, user) + } } } diff --git a/main_test.go b/main_test.go index 1a05b1b..10bdfc9 100644 --- a/main_test.go +++ b/main_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -155,14 +156,14 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) formData := url.Values{ - "email": {"mail@mail.de"}, - "password": {"password"}, - "csrf-token": {"invalid-csrf-token"}, + "email": {"mail@mail.de"}, + "password": {"password"}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("csrf-token", "invalid-csrf-token") resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) @@ -187,20 +188,20 @@ func TestIntegrationAuth(t *testing.T) { html, err := html.Parse(resp.Body) assert.Nil(t, err) - csrfToken := findCsrfToken(html) + csrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", csrfToken) session := findCookie(resp, "id") formData := url.Values{ - "email": {"invalid@mail.de"}, - "password": {"password"}, - "csrf-token": {csrfToken}, + "email": {"invalid@mail.de"}, + "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+session.Value) + req.Header.Set("csrf-token", csrfToken) req.Header.Set("HX-Request", "true") resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -227,14 +228,13 @@ func TestIntegrationAuth(t *testing.T) { html, err := html.Parse(resp.Body) assert.Nil(t, err) - csrfToken := findCsrfToken(html) + csrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", csrfToken) session := findCookie(resp, "id") formData := url.Values{ - "email": {"mail@mail.de"}, - "password": {"invalid-password"}, - "csrf-token": {csrfToken}, + "email": {"mail@mail.de"}, + "password": {"invalid-password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) @@ -242,6 +242,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+session.Value) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) @@ -267,20 +268,20 @@ func TestIntegrationAuth(t *testing.T) { html, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(html) + anonymousCsrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSession := findCookie(resp, "id") assert.NotNil(t, anonymousSession) formData := url.Values{ - "email": {"mail@mail.de"}, - "password": {"password"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"mail@mail.de"}, + "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("csrf-token", anonymousCsrfToken) req.Header.Set("Cookie", "id="+anonymousSession.Value) resp, err = httpClient.Do(req) @@ -314,21 +315,21 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) body, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(body) + anonymousCsrfToken := findCsrfToken(t, body) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSession := findCookie(resp, "id") assert.NotNil(t, anonymousSession) formData := url.Values{ - "email": {"mail@mail.de"}, - "password": {"password"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"mail@mail.de"}, + "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSession.Value) + req.Header.Set("csrf-token", anonymousCsrfToken) req.Header.Set("HX-Request", "true") timeStart := time.Now() @@ -348,15 +349,14 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) body, err = html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken = findCsrfToken(body) + anonymousCsrfToken = findCsrfToken(t, body) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSession = findCookie(resp, "id") assert.NotNil(t, anonymousSession) formData = url.Values{ - "email": {"mail@mail.de"}, - "password": {"wrong-password"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"mail@mail.de"}, + "password": {"wrong-password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) @@ -364,6 +364,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", anonymousCsrfToken) timeStart = time.Now() resp, err = httpClient.Do(req) @@ -382,15 +383,14 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) body, err = html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken = findCsrfToken(body) + anonymousCsrfToken = findCsrfToken(t, body) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSession = findCookie(resp, "id") assert.NotNil(t, anonymousSession) formData = url.Values{ - "email": {"invalid-mail@mail.de"}, - "password": {"password"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"invalid-mail@mail.de"}, + "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) @@ -398,6 +398,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", anonymousCsrfToken) timeStart = time.Now() resp, err = httpClient.Do(req) @@ -427,15 +428,14 @@ func TestIntegrationAuth(t *testing.T) { html, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(html) + anonymousCsrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSession := findCookie(resp, "id") assert.NotNil(t, anonymousSession) formData := url.Values{ - "email": {"mail@mail.de"}, - "password": {"password"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"mail@mail.de"}, + "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) @@ -443,6 +443,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSession.Value) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", anonymousCsrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -491,14 +492,14 @@ func TestIntegrationAuth(t *testing.T) { _, basePath, ctx := setupIntegrationTest(t) formData := url.Values{ - "email": {"mail@mail.de"}, - "password": {"password"}, - "csrf-token": {"invalid-csrf-token"}, + "email": {"mail@mail.de"}, + "password": {"password"}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signin", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", "invalid-csrf-token") resp, err := httpClient.Do(req) assert.Nil(t, err) @@ -516,21 +517,21 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) body, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(body) + anonymousCsrfToken := findCsrfToken(t, body) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSession := findCookie(resp, "id") assert.NotNil(t, anonymousSession) formData := url.Values{ - "email": {"mail@mail.de"}, - "password": {"insecure-password"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"mail@mail.de"}, + "password": {"insecure-password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signup", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") req.Header.Set("Cookie", "id="+anonymousSession.Value) + req.Header.Set("csrf-token", anonymousCsrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -553,21 +554,21 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) body, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(body) + anonymousCsrfToken := findCsrfToken(t, body) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSession := findCookie(resp, "id") assert.NotNil(t, anonymousSession) formData := url.Values{ - "email": {"mail@mail.de"}, - "password": {"secure-Password!1"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"mail@mail.de"}, + "password": {"secure-Password!1"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signup", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") req.Header.Set("Cookie", "id="+anonymousSession.Value) + req.Header.Set("csrf-token", anonymousCsrfToken) timeStart := time.Now() resp, err = httpClient.Do(req) timeEnd := time.Now() @@ -590,21 +591,21 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) body, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(body) + anonymousCsrfToken := findCsrfToken(t, body) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSession := findCookie(resp, "id") assert.NotNil(t, anonymousSession) formData := url.Values{ - "email": {"mail@mail.de"}, - "password": {"secure-Password!1"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"mail@mail.de"}, + "password": {"secure-Password!1"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/signup", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") req.Header.Set("Cookie", "id="+anonymousSession.Value) + req.Header.Set("csrf-token", anonymousCsrfToken) timeStart := time.Now() resp, err = httpClient.Do(req) timeEnd := time.Now() @@ -826,12 +827,11 @@ func TestIntegrationAuth(t *testing.T) { html, err := html.Parse(resp.Body) assert.Nil(t, err) - csrfToken := findCsrfToken(html) + csrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", csrfToken) formData := url.Values{ - "password": {"wrong-password"}, - "csrf-token": {csrfToken}, + "password": {"wrong-password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/delete-account", strings.NewReader(formData.Encode())) @@ -839,6 +839,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -863,8 +864,7 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) formData := url.Values{ - "password": {"password"}, - "csrf-token": {"wrong-csrf-token"}, + "password": {"password"}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/delete-account", strings.NewReader(formData.Encode())) @@ -872,6 +872,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", "wrong-csrf-token") resp, err := httpClient.Do(req) assert.Nil(t, err) @@ -885,54 +886,54 @@ func TestIntegrationAuth(t *testing.T) { userId, csrfToken, sessionId := createValidUserSession(t, db, ctx, basePath, "") formData := url.Values{ - "name": {"Name"}, - "csrf-token": {csrfToken}, + "name": {"Name"}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/account/new", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) + req.Header.Set("csrf-token", csrfToken) req.Header.Set("HX-Request", "true") resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) formData = url.Values{ - "name": {"Name"}, - "csrf-token": {csrfToken}, + "name": {"Name"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/treasurechest/new", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) formData = url.Values{ - "timestamp": {"2006-01-02"}, - "value": {"100.00"}, - "csrf-token": {csrfToken}, + "timestamp": {"2006-01-02"}, + "value": {"100.00"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/transaction/new", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) formData = url.Values{ - "password": {"password"}, - "csrf-token": {csrfToken}, + "password": {"password"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/delete-account", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -985,7 +986,7 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) html, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(html) + anonymousCsrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", anonymousCsrfToken) anonymousSessionId := findCookie(resp, "id").Value assert.NotEqual(t, "", anonymousSessionId) @@ -993,13 +994,13 @@ func TestIntegrationAuth(t *testing.T) { formData := url.Values{ "current-password": {"password"}, "new-password": {"MyNewSecurePassword1!"}, - "csrf-token": {anonymousCsrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", anonymousCsrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -1026,7 +1027,6 @@ func TestIntegrationAuth(t *testing.T) { formData := url.Values{ "current-password": {"password"}, "new-password": {"MyNewSecurePassword1!"}, - "csrf-token": {"invalid-csrf-token"}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) @@ -1034,6 +1034,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", "invalid-csrf-token") resp, err := httpClient.Do(req) assert.Nil(t, err) @@ -1069,19 +1070,19 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) html, err := html.Parse(resp.Body) assert.Nil(t, err) - csrfToken := findCsrfToken(html) + csrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", csrfToken) formData := url.Values{ "current-password": {"wrong-password"}, "new-password": {"MyNewSecurePassword1!"}, - "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -1117,19 +1118,19 @@ func TestIntegrationAuth(t *testing.T) { assert.Nil(t, err) html, err := html.Parse(resp.Body) assert.Nil(t, err) - csrfToken := findCsrfToken(html) + csrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", csrfToken) formData := url.Values{ "current-password": {"password"}, "new-password": {"insecure-password"}, - "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -1176,13 +1177,12 @@ func TestIntegrationAuth(t *testing.T) { html, err := html.Parse(resp.Body) assert.Nil(t, err) - csrfToken := findCsrfToken(html) + csrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", csrfToken) formData := url.Values{ "current-password": {"password"}, "new-password": {"MyNewSecurePassword1!"}, - "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) @@ -1190,6 +1190,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -1266,13 +1267,13 @@ func TestIntegrationAuth(t *testing.T) { assert.NotEqual(t, "", anonymousSessionId) formData := url.Values{ - "email": {"mail@mail.de"}, - "csrf-token": {"invalid-csrf-token"}, + "email": {"mail@mail.de"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", "invalid-csrf-token") resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -1297,17 +1298,17 @@ func TestIntegrationAuth(t *testing.T) { assert.NotEqual(t, "", anonymousSessionId) body, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(body) + anonymousCsrfToken := findCsrfToken(t, body) formData := url.Values{ - "email": {"non-existent@mail.de"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"non-existent@mail.de"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") req.Header.Set("Cookie", "id="+anonymousSessionId) + req.Header.Set("csrf-token", anonymousCsrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -1337,17 +1338,17 @@ func TestIntegrationAuth(t *testing.T) { assert.NotEqual(t, "", anonymousSessionId) body, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(body) + anonymousCsrfToken := findCsrfToken(t, body) formData := url.Values{ - "email": {"mail@mail.de"}, - "csrf-token": {anonymousCsrfToken}, + "email": {"mail@mail.de"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") req.Header.Set("Cookie", "id="+anonymousSessionId) + req.Header.Set("csrf-token", anonymousCsrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -1383,12 +1384,11 @@ func TestIntegrationAuth(t *testing.T) { anonymousSessionId := findCookie(resp, "id").Value html, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(html) + anonymousCsrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", anonymousCsrfToken) formData := url.Values{ "new-password": {"MyNewSecurePassword1!"}, - "csrf-token": {anonymousCsrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) assert.Nil(t, err) @@ -1396,6 +1396,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("HX-Request", "true") req.Header.Set("HX-Current-URL", basePath+"/auth/change-password?token=invalidToken") + req.Header.Set("csrf-token", anonymousCsrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) @@ -1424,7 +1425,7 @@ func TestIntegrationAuth(t *testing.T) { anonymousSessionId := findCookie(resp, "id").Value html, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(html) + anonymousCsrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", anonymousCsrfToken) token := "password-reset-token" @@ -1435,7 +1436,6 @@ func TestIntegrationAuth(t *testing.T) { formData := url.Values{ "new-password": {"MyNewSecurePassword1!"}, - "csrf-token": {anonymousCsrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) assert.Nil(t, err) @@ -1443,6 +1443,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("HX-Request", "true") req.Header.Set("HX-Current-URL", basePath+"/auth/change-password?token="+url.QueryEscape(token)) + req.Header.Set("csrf-token", anonymousCsrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) @@ -1471,7 +1472,7 @@ func TestIntegrationAuth(t *testing.T) { anonymousSessionId := findCookie(resp, "id").Value html, err := html.Parse(resp.Body) assert.Nil(t, err) - anonymousCsrfToken := findCsrfToken(html) + anonymousCsrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", anonymousCsrfToken) token := "password-reset-token" @@ -1482,7 +1483,6 @@ func TestIntegrationAuth(t *testing.T) { formData := url.Values{ "new-password": {"insecure-password"}, - "csrf-token": {anonymousCsrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) assert.Nil(t, err) @@ -1490,6 +1490,7 @@ func TestIntegrationAuth(t *testing.T) { req.Header.Set("Cookie", "id="+anonymousSessionId) req.Header.Set("HX-Request", "true") req.Header.Set("HX-Current-URL", basePath+"/auth/change-password?token="+url.QueryEscape(token)) + req.Header.Set("csrf-token", anonymousCsrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) @@ -1524,18 +1525,18 @@ func TestIntegrationAuth(t *testing.T) { sessionId := findCookie(resp, "id").Value html, err := html.Parse(resp.Body) assert.Nil(t, err) - csrfToken := findCsrfToken(html) + csrfToken := findCsrfToken(t, html) assert.NotEqual(t, "", csrfToken) formData := url.Values{ - "email": {"mail@mail.de"}, - "csrf-token": {csrfToken}, + "email": {"mail@mail.de"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -1546,13 +1547,13 @@ func TestIntegrationAuth(t *testing.T) { formData = url.Values{ "new-password": {"MyNewSecurePassword1!"}, - "csrf-token": {csrfToken}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("HX-Request", "true") + req.Header.Set("csrf-token", csrfToken) req.Header.Set("HX-Current-URL", basePath+"/auth/change-password?token="+url.QueryEscape(token)) resp, err = httpClient.Do(req) assert.Nil(t, err) @@ -1667,13 +1668,13 @@ func TestIntegrationAccount(t *testing.T) { assert.Equal(t, "/auth/signin", resp.Header.Get("Location")) formData := url.Values{ - "name": {"name"}, - "csrf-token": {csrfToken}, + "name": {"name"}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/account/some-id", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusSeeOther, resp.StatusCode) @@ -1698,13 +1699,13 @@ func TestIntegrationAccount(t *testing.T) { // Insert expectedName := "My great Account" formData := url.Values{ - "name": {expectedName}, - "csrf-token": {csrfToken}, + "name": {expectedName}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/account/new", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) + req.Header.Set("csrf-token", csrfToken) resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -1717,13 +1718,13 @@ func TestIntegrationAccount(t *testing.T) { // Update expectedNewName := "My new Account" formData = url.Values{ - "name": {expectedNewName}, - "csrf-token": {csrfToken}, + "name": {expectedNewName}, } req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/account/"+id.String(), strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) + req.Header.Set("csrf-token", csrfToken) resp, err = httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -1768,13 +1769,13 @@ func TestIntegrationAccount(t *testing.T) { expectedName1 := "Account 1" formData := url.Values{ - "name": {expectedName1}, - "csrf-token": {csrfToken1}, + "name": {expectedName1}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/account/new", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId1) + req.Header.Set("csrf-token", csrfToken1) resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -1809,14 +1810,14 @@ func TestIntegrationAccount(t *testing.T) { for name, status := range data { formData := url.Values{ - "name": {name}, - "csrf-token": {csrfToken}, + "name": {name}, } req, err := http.NewRequestWithContext(ctx, "POST", basePath+"/account/new", strings.NewReader(formData.Encode())) assert.Nil(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "id="+sessionId) + req.Header.Set("csrf-token", csrfToken) resp, err := httpClient.Do(req) assert.Nil(t, err) assert.Equal(t, status, resp.StatusCode, "for name: "+name) @@ -1859,7 +1860,7 @@ func createAnonymousSession(t *testing.T, ctx context.Context, basePath string) html, err := html.Parse(resp.Body) assert.Nil(t, err) - return findCsrfToken(html), findCookie(resp, "id").Value + return findCsrfToken(t, html), findCookie(resp, "id").Value } func findCookie(resp *http.Response, name string) *http.Cookie { @@ -1958,19 +1959,19 @@ func waitForReady( } } -func findCsrfToken(data *html.Node) string { - attr := getTokenAttribute(data) - if attr != nil { - return attr.Val +func findCsrfToken(t *testing.T, data *html.Node) string { + token := getTokenAttribute(t, data) + if token != "" { + return token } if data.FirstChild != nil { - if token := findCsrfToken(data.FirstChild); token != "" { + if token = findCsrfToken(t, data.FirstChild); token != "" { return token } } if data.NextSibling != nil { - if token := findCsrfToken(data.NextSibling); token != "" { + if token = findCsrfToken(t, data.NextSibling); token != "" { return token } } @@ -1978,25 +1979,16 @@ func findCsrfToken(data *html.Node) string { return "" } -func getTokenAttribute(data *html.Node) *html.Attribute { - returnValue := false +func getTokenAttribute(t *testing.T, data *html.Node) string { for _, attr := range data.Attr { - if attr.Key == "name" && attr.Val == "csrf-token" { - returnValue = true + if attr.Key == "hx-headers" { + var data map[string]interface{} + err := json.Unmarshal([]byte(attr.Val), &data) + assert.Nil(t, err) + return data["csrf-token"].(string) } } - - if !returnValue { - return nil - } - - for _, attr := range data.Attr { - if attr.Key == "value" { - return &attr - } - } - - return nil + return "" } func readBody(t *testing.T, body io.ReadCloser) string { diff --git a/service/transaction.go b/service/transaction.go index 4b789f9..b90a957 100644 --- a/service/transaction.go +++ b/service/transaction.go @@ -29,7 +29,7 @@ type Transaction interface { Add(user *types.User, transaction types.TransactionInput) (*types.Transaction, error) Update(user *types.User, transaction types.TransactionInput) (*types.Transaction, error) Get(user *types.User, id string) (*types.Transaction, error) - GetAll(user *types.User) ([]*types.Transaction, error) + GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) Delete(user *types.User, id string) error RecalculateBalances(user *types.User) error @@ -241,13 +241,20 @@ func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, e return &transaction, nil } -func (s TransactionImpl) GetAll(user *types.User) ([]*types.Transaction, error) { +func (s TransactionImpl) GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) { transactionMetric.WithLabelValues("get_all").Inc() if user == nil { return nil, ErrUnauthorized } + transactions := make([]*types.Transaction, 0) - err := s.db.Select(&transactions, `SELECT * FROM "transaction" WHERE user_id = ? ORDER BY timestamp DESC`, user.Id) + err := s.db.Select(&transactions, ` + SELECT * + FROM "transaction" + WHERE user_id = ? + AND (? = '' OR account_id = ?) + AND (? = '' OR treasure_chest_id = ?) + ORDER BY timestamp DESC`, user.Id, filter.AccountId, filter.AccountId, filter.TreasureChestId, filter.TreasureChestId) err = db.TransformAndLogDbError("transaction GetAll", nil, err) if err != nil { return nil, err diff --git a/template/transaction/transaction.templ b/template/transaction/transaction.templ index 1241eda..f4a34eb 100644 --- a/template/transaction/transaction.templ +++ b/template/transaction/transaction.templ @@ -6,23 +6,61 @@ import "spend-sparrow/template/svg" import "spend-sparrow/types" import "github.com/google/uuid" -templ Transaction(transactions []*types.Transaction, accounts, treasureChests map[uuid.UUID]string) { - {{ }} +templ Transaction(items templ.Component, filter types.TransactionItemsFilter, accounts []*types.Account, treasureChests []*types.TreasureChest) {