From 9a61453e866630fdd18ddf377ce19711bafe1c83 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:38:32 +0100 Subject: [PATCH] fix(deps): update module github.com/labstack/echo/v4 to v5 (#2131) Closes https://github.com/go-vikunja/vikunja/pull/2133 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: kolaente --- examples/plugins/example/main.go | 6 +- go.mod | 8 - go.sum | 26 +-- pkg/cmd/web.go | 79 +++++--- pkg/log/echo_logger.go | 167 +---------------- pkg/log/logging.go | 19 +- pkg/log/mail_logger.go | 2 +- pkg/log/watermill_logger.go | 2 +- pkg/log/xorm_logger.go | 2 +- pkg/models/api_routes.go | 162 +++++++++++----- pkg/models/task_collection.go | 2 +- pkg/models/tasks.go | 2 +- pkg/modules/auth/auth.go | 8 +- pkg/modules/auth/openid/openid.go | 6 +- pkg/modules/background/handler/background.go | 38 ++-- pkg/modules/background/unsplash/proxy.go | 8 +- pkg/modules/migration/handler/common.go | 4 +- pkg/modules/migration/handler/handler.go | 10 +- pkg/modules/migration/handler/handler_file.go | 6 +- pkg/plugins/interfaces.go | 2 +- pkg/plugins/manager.go | 2 +- pkg/routes/api/v1/avatar.go | 6 +- pkg/routes/api/v1/docs.go | 8 +- pkg/routes/api/v1/info.go | 4 +- pkg/routes/api/v1/link_sharing_auth.go | 4 +- pkg/routes/api/v1/login.go | 6 +- pkg/routes/api/v1/notifications.go | 4 +- pkg/routes/api/v1/task_attachment.go | 14 +- pkg/routes/api/v1/testing.go | 4 +- pkg/routes/api/v1/token_check.go | 6 +- pkg/routes/api/v1/user_caldav_token.go | 8 +- pkg/routes/api/v1/user_confirm_email.go | 6 +- pkg/routes/api/v1/user_deletion.go | 20 +- pkg/routes/api/v1/user_export.go | 14 +- pkg/routes/api/v1/user_list.go | 6 +- pkg/routes/api/v1/user_password_reset.go | 12 +- pkg/routes/api/v1/user_register.go | 4 +- pkg/routes/api/v1/user_settings.go | 14 +- pkg/routes/api/v1/user_show.go | 6 +- pkg/routes/api/v1/user_totp.go | 14 +- pkg/routes/api/v1/user_update_email.go | 4 +- pkg/routes/api/v1/user_update_password.go | 8 +- pkg/routes/api/v1/webhooks.go | 4 +- pkg/routes/api_tokens.go | 27 +-- pkg/routes/caldav/auth.go | 4 +- pkg/routes/caldav/handler.go | 22 +-- pkg/routes/error_handler.go | 52 +++--- pkg/routes/healthcheck.go | 6 +- pkg/routes/metrics.go | 10 +- pkg/routes/rate_limit.go | 12 +- pkg/routes/routes.go | 176 ++++++++++++------ pkg/routes/sentry_middleware.go | 85 +++++++++ pkg/routes/static.go | 12 +- pkg/user/user.go | 6 +- pkg/web/handler/create.go | 8 +- pkg/web/handler/delete.go | 8 +- pkg/web/handler/read_all.go | 10 +- pkg/web/handler/read_one.go | 6 +- pkg/web/handler/update.go | 8 +- pkg/web/web.go | 2 +- pkg/webtests/api_tokens_test.go | 28 +-- pkg/webtests/integrations.go | 63 ++++--- pkg/webtests/task_attachment_upload_test.go | 2 +- 63 files changed, 667 insertions(+), 617 deletions(-) create mode 100644 pkg/routes/sentry_middleware.go diff --git a/examples/plugins/example/main.go b/examples/plugins/example/main.go index 81ca34b1e..f791f0b02 100644 --- a/examples/plugins/example/main.go +++ b/examples/plugins/example/main.go @@ -27,7 +27,7 @@ import ( "code.vikunja.io/api/pkg/user" "github.com/ThreeDotsLabs/watermill/message" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type ExamplePlugin struct{} @@ -58,7 +58,7 @@ func (p *ExamplePlugin) RegisterUnauthenticatedRoutes(g *echo.Group) { } // Authenticated route handlers -func handleUserInfo(c echo.Context) error { +func handleUserInfo(c *echo.Context) error { s := db.NewSession() defer s.Close() @@ -80,7 +80,7 @@ func handleUserInfo(c echo.Context) error { } // Unauthenticated route handlers -func handleStatus(c echo.Context) error { +func handleStatus(c *echo.Context) error { p := &ExamplePlugin{} diff --git a/go.mod b/go.mod index 385a519c8..48ca38aa4 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ require ( github.com/gabriel-vasile/mimetype v1.4.12 github.com/ganigeorgiev/fexpr v0.5.0 github.com/getsentry/sentry-go v0.41.0 - github.com/getsentry/sentry-go/echo v0.41.0 github.com/go-ldap/ldap/v3 v3.4.12 github.com/go-sql-driver/mysql v1.9.3 github.com/go-testfixtures/testfixtures/v3 v3.19.0 @@ -51,11 +50,8 @@ require ( github.com/jaswdr/faker/v2 v2.9.1 github.com/jinzhu/copier v0.4.0 github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 - github.com/labstack/echo-jwt/v4 v4.4.0 github.com/labstack/echo-jwt/v5 v5.0.0 - github.com/labstack/echo/v4 v4.15.0 github.com/labstack/echo/v5 v5.0.0 - github.com/labstack/gommon v0.4.2 github.com/lib/pq v1.10.9 github.com/magefile/mage v1.15.0 github.com/mattn/go-sqlite3 v1.14.33 @@ -172,8 +168,6 @@ require ( github.com/syndtr/goleveldb v1.0.0 // indirect github.com/tj/assert v0.0.3 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect go.uber.org/mock v0.5.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect @@ -189,8 +183,6 @@ require ( replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7 -replace github.com/labstack/echo/v4 => github.com/kolaente/echo/v4 v4.0.0-20250124112709-682dfde74c31 // https://github.com/labstack/echo/pull/2738 - go 1.25.0 toolchain go1.25.6 diff --git a/go.sum b/go.sum index 447fb1620..b1abf98db 100644 --- a/go.sum +++ b/go.sum @@ -94,8 +94,6 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= -github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= @@ -132,8 +130,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b h1:+0Xqob+onh+4l9TSWmFyZ4JHqGUiCy5P1muyH8Evfpw= github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fclairamb/afero-s3 v0.4.0 h1:N++eKFyOTkdYQMDSAU2AGMVWBOo49FqbpeQ+e3v1jQA= @@ -148,14 +144,8 @@ github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCK github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk= github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE= -github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo= -github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= github.com/getsentry/sentry-go v0.41.0 h1:q/dQZOlEIb4lhxQSjJhQqtRr3vwrJ6Ahe1C9zv+ryRo= github.com/getsentry/sentry-go v0.41.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= -github.com/getsentry/sentry-go/echo v0.40.0 h1:6vAmqHZbloXwGmESjtTqroti+MI8odvXtEo6PSOP0r0= -github.com/getsentry/sentry-go/echo v0.40.0/go.mod h1:UOd1hu1AlkrJrUm5vJtWfg4k/fnPRxkiDm3gxpNQ6cs= -github.com/getsentry/sentry-go/echo v0.41.0 h1:f4dL3KlNI8iTD+30dUQEWG/NTJnWEkqalV2Lw90sX40= -github.com/getsentry/sentry-go/echo v0.41.0/go.mod h1:qfdk4TD+SIrtwOlYWW+3TAX+taksREddhzaKCe1+2eQ= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= @@ -324,8 +314,6 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible h1:q7DbyV+sFjEoTuuUdRDNl2nlyfztkZgxVVCV7JhzIkY= github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw= -github.com/kolaente/echo/v4 v4.0.0-20250124112709-682dfde74c31 h1:lUUZppO9AB30mfNALYcMAbr32XJtZG4itqG21crNAlQ= -github.com/kolaente/echo/v4 v4.0.0-20250124112709-682dfde74c31/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -338,12 +326,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/labstack/echo-jwt/v4 v4.4.0 h1:nrXaEnJupfc2R4XChcLRDyghhMZup77F8nIzHnBK19U= -github.com/labstack/echo-jwt/v4 v4.4.0/go.mod h1:kYXWgWms9iFqI3ldR+HAEj/Zfg5rZtR7ePOgktG4Hjg= +github.com/labstack/echo-jwt/v5 v5.0.0 h1:uPp+FpkI/PKpMPPygtnK3RQOpg5a2wlM04UgfpWLVyI= github.com/labstack/echo-jwt/v5 v5.0.0/go.mod h1:RYF2ojWXbaY09QQ5J9vVtPUtkyI5UztS0gJotmCRz/U= +github.com/labstack/echo/v5 v5.0.0 h1:JHKGrI0cbNsNMyKvranuY0C94O4hSM7yc/HtwcV3Na4= github.com/labstack/echo/v5 v5.0.0/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef h1:RZnRnSID1skF35j/15KJ6hKZkdIC/teQClJK5wP5LU4= github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -402,12 +388,8 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg= -github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM= github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= -github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc= -github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg= github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -515,10 +497,6 @@ github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2t github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts= diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index d4dde5953..aac06fef9 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -18,7 +18,10 @@ package cmd import ( "context" + "crypto/tls" + "errors" "net" + "net/http" "net/url" "os" "os/signal" @@ -35,7 +38,6 @@ import ( "code.vikunja.io/api/pkg/utils" "code.vikunja.io/api/pkg/version" - "github.com/labstack/echo/v4" "github.com/spf13/cobra" "golang.org/x/crypto/acme/autocert" ) @@ -44,12 +46,12 @@ func init() { rootCmd.AddCommand(webCmd) } -func setupUnixSocket(e *echo.Echo) error { +func setupUnixSocket() (net.Listener, error) { path := config.ServiceUnixSocket.GetString() // Remove old unix socket that may have remained after a crash if err := os.Remove(path); err != nil && !os.IsNotExist(err) { - return err + return nil, err } if config.ServiceUnixSocketMode.Get() != nil { @@ -61,16 +63,10 @@ func setupUnixSocket(e *echo.Echo) error { } cfg := net.ListenConfig{} - l, err := cfg.Listen(context.Background(), "unix", path) - if err != nil { - return err - } - - e.Listener = l - return nil + return cfg.Listen(context.Background(), "unix", path) } -func setupAutoTLS(e *echo.Echo) { +func setupAutoTLS(server *http.Server) { if config.ServiceUnixSocket.GetString() != "" { log.Warning("Auto tls is enabled but listening on a unix socket is enabled as well. The latter will be ignored.") } @@ -95,7 +91,8 @@ func setupAutoTLS(e *echo.Echo) { if config.AutoTLSEmail.GetString() == "" { log.Fatalf("You must provide an email address to use autotls.") } - e.AutoTLSManager = autocert.Manager{ + + manager := autocert.Manager{ Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(filepath.Join( config.FilesBasePath.GetString(), @@ -106,14 +103,34 @@ func setupAutoTLS(e *echo.Echo) { Email: config.AutoTLSEmail.GetString(), } + server.TLSConfig = &tls.Config{ + GetCertificate: manager.GetCertificate, + NextProtos: []string{"h2", "http/1.1", "acme-tls/1"}, + MinVersion: tls.VersionTLS12, + } + if config.ServiceInterface.GetString() != ":443" { log.Warningf("Vikunja's interface is set to %s, with tls it is recommended to set this to :443", config.ServiceInterface.GetString()) } - err = e.StartAutoTLS(config.ServiceInterface.GetString()) - if err != nil { - e.Logger.Info("shutting down...") + // Start HTTP server for ACME challenges + go func() { + httpServer := &http.Server{ + Addr: ":http", + Handler: manager.HTTPHandler(nil), + ReadHeaderTimeout: 10 * time.Second, + } + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Errorf("HTTP server for ACME failed: %v", err) + } + }() + + log.Infof("HTTPS server listening on %s", config.ServiceInterface.GetString()) + err = server.ListenAndServeTLS("", "") + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Errorf("Server error: %v", err) } + log.Info("shutting down...") } var webCmd = &cobra.Command{ @@ -130,24 +147,40 @@ var webCmd = &cobra.Command{ // Start the webserver e := routes.NewEcho() routes.RegisterRoutes(e) + + // Create HTTP server with Echo as handler + server := &http.Server{ + Addr: config.ServiceInterface.GetString(), + Handler: e, + ReadHeaderTimeout: 10 * time.Second, + } + // Start server go func() { if config.AutoTLSEnabled.GetBool() { - setupAutoTLS(e) + setupAutoTLS(server) return } + var err error // Listen unix socket if needed (ServiceInterface will be ignored) if config.ServiceUnixSocket.GetString() != "" { - if err := setupUnixSocket(e); err != nil { - e.Logger.Fatal(err) + var listener net.Listener + listener, err = setupUnixSocket() + if err != nil { + log.Fatalf("Failed to setup unix socket: %v", err) } + log.Infof("HTTP server listening on unix socket %s", config.ServiceUnixSocket.GetString()) + err = server.Serve(listener) + } else { + log.Infof("HTTP server listening on %s", config.ServiceInterface.GetString()) + err = server.ListenAndServe() } - err := e.Start(config.ServiceInterface.GetString()) - if err != nil { - e.Logger.Info("shutting down...") + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Errorf("Server error: %v", err) } + log.Info("shutting down...") }() // Wait for interrupt signal to gracefully shut down the server with @@ -158,8 +191,8 @@ var webCmd = &cobra.Command{ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() log.Infof("Shutting down...") - if err := e.Shutdown(ctx); err != nil { - e.Logger.Fatal(err) + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("Server shutdown failed: %v", err) } cron.Stop() plugins.Shutdown() diff --git a/pkg/log/echo_logger.go b/pkg/log/echo_logger.go index fe298fe51..ddc0e0724 100644 --- a/pkg/log/echo_logger.go +++ b/pkg/log/echo_logger.go @@ -17,170 +17,11 @@ package log import ( - "encoding/json" - "fmt" - "io" "log/slog" - "os" - - "github.com/labstack/echo/v4" - "github.com/labstack/gommon/log" ) -type EchoLogger struct { - logger *slog.Logger - writer io.Writer -} - -// NewEchoLogger creates and initializes a new echo logger -func NewEchoLogger(configLogEnabled bool, configLogEcho string, configLogFormat string) echo.Logger { - handler, writer := makeLogHandler(configLogEnabled, configLogEcho, "DEBUG", configLogFormat) - - echoLogger := &EchoLogger{ - logger: slog.New(handler).With("component", "http"), - writer: writer, - } - - return echoLogger -} - -func (e *EchoLogger) Output() io.Writer { - return e.writer -} - -func (e *EchoLogger) SetOutput(_ io.Writer) { -} - -func (e *EchoLogger) Prefix() string { - return "http" -} - -func (e *EchoLogger) SetPrefix(_ string) { -} - -func (e *EchoLogger) Level() log.Lvl { - return log.DEBUG -} - -func (e *EchoLogger) SetLevel(_ log.Lvl) { -} - -func (e *EchoLogger) SetHeader(_ string) { -} - -func (e *EchoLogger) Print(i ...interface{}) { - e.logger.Info(fmt.Sprint(i...)) -} - -func (e *EchoLogger) Printf(format string, args ...interface{}) { - e.logger.Info(fmt.Sprintf(format, args...)) -} - -func (e *EchoLogger) Printj(j log.JSON) { - if b, err := json.Marshal(j); err == nil { - e.logger.Info(string(b)) - } -} - -func (e *EchoLogger) Debug(i ...interface{}) { - e.logger.Debug(fmt.Sprint(i...)) -} - -func (e *EchoLogger) Debugf(format string, args ...interface{}) { - e.logger.Debug(fmt.Sprintf(format, args...)) -} - -func (e *EchoLogger) Debugj(j log.JSON) { - if b, err := json.Marshal(j); err == nil { - e.logger.Debug(string(b)) - } -} - -func (e *EchoLogger) Info(i ...interface{}) { - e.logger.Info(fmt.Sprint(i...)) -} - -func (e *EchoLogger) Infof(format string, args ...interface{}) { - e.logger.Info(fmt.Sprintf(format, args...)) -} - -func (e *EchoLogger) Infoj(j log.JSON) { - if b, err := json.Marshal(j); err == nil { - e.logger.Info(string(b)) - } -} - -func (e *EchoLogger) Warn(i ...interface{}) { - e.logger.Warn(fmt.Sprint(i...)) -} - -func (e *EchoLogger) Warnf(format string, args ...interface{}) { - e.logger.Warn(fmt.Sprintf(format, args...)) -} - -func (e *EchoLogger) Warnj(j log.JSON) { - if b, err := json.Marshal(j); err == nil { - e.logger.Warn(string(b)) - } -} - -func (e *EchoLogger) Error(i ...interface{}) { - e.logger.Error(fmt.Sprint(i...)) -} - -func (e *EchoLogger) Errorf(format string, args ...interface{}) { - e.logger.Error(fmt.Sprintf(format, args...)) -} - -func (e *EchoLogger) Errorj(j log.JSON) { - if b, err := json.Marshal(j); err == nil { - e.logger.Error(string(b)) - } -} - -func (e *EchoLogger) Fatal(i ...interface{}) { - e.logger.Error(fmt.Sprint(i...)) - os.Exit(1) -} - -func (e *EchoLogger) Fatalj(j log.JSON) { - if b, err := json.Marshal(j); err == nil { - e.logger.Error(string(b)) - } - os.Exit(1) -} - -func (e *EchoLogger) Fatalf(format string, args ...interface{}) { - e.logger.Error(fmt.Sprintf(format, args...)) - os.Exit(1) -} - -func (e *EchoLogger) Panic(i ...interface{}) { - msg := fmt.Sprint(i...) - e.logger.Error(msg) - panic(msg) -} - -func (e *EchoLogger) Panicj(j log.JSON) { - if b, err := json.Marshal(j); err == nil { - msg := string(b) - e.logger.Error(msg) - panic(msg) - } -} - -func (e *EchoLogger) Panicf(format string, args ...interface{}) { - msg := fmt.Sprintf(format, args...) - e.logger.Error(msg) - panic(msg) -} - -// EnableColor enables color output -func (e *EchoLogger) EnableColor() { - // This is a no-op for our slog implementation -} - -// DisableColor disables color output -func (e *EchoLogger) DisableColor() { - // This is a no-op for our slog implementation +// NewEchoLogger creates and initializes a new slog logger for Echo v5 +func NewEchoLogger(configLogEnabled bool, configLogEcho string, configLogFormat string) *slog.Logger { + handler := makeLogHandler(configLogEnabled, configLogEcho, "DEBUG", configLogFormat) + return slog.New(handler).With("component", "http") } diff --git a/pkg/log/logging.go b/pkg/log/logging.go index e3bb6b098..4d094f614 100644 --- a/pkg/log/logging.go +++ b/pkg/log/logging.go @@ -37,7 +37,7 @@ func InitLogger() { logInstance = slog.New(handler) } -func makeLogHandler(enabled bool, output string, level string, format string) (slog.Handler, io.Writer) { +func makeLogHandler(enabled bool, output string, level string, format string) slog.Handler { var slogLevel slog.Level switch strings.ToUpper(level) { case "CRITICAL", "ERROR": @@ -65,12 +65,21 @@ func makeLogHandler(enabled bool, output string, level string, format string) (s writer = getLogWriter(output, "standard") } - return createHandler(writer, slogLevel, format), writer + return createHandler(writer, slogLevel, format) } // createHandler creates a consistent slog handler for all loggers func createHandler(writer io.Writer, level slog.Level, format string) slog.Handler { - handlerOpts := &slog.HandlerOptions{Level: level} + handlerOpts := &slog.HandlerOptions{ + Level: level, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + // Remove message attribute when empty + if a.Key == slog.MessageKey && a.Value.String() == "" { + return slog.Attr{} + } + return a + }, + } if strings.ToLower(format) == "structured" { return slog.NewJSONHandler(writer, handlerOpts) } @@ -80,7 +89,7 @@ func createHandler(writer io.Writer, level slog.Level, format string) slog.Handl // NewHTTPLogger creates and initializes a new HTTP logger func NewHTTPLogger(enabled bool, output string, format string) *slog.Logger { - handler, _ := makeLogHandler(enabled, output, "DEBUG", format) + handler := makeLogHandler(enabled, output, "DEBUG", format) return slog.New(handler).With("component", "http") } @@ -88,7 +97,7 @@ func NewHTTPLogger(enabled bool, output string, format string) *slog.Logger { // ConfigureStandardLogger configures the global log handler func ConfigureStandardLogger(enabled bool, output string, path string, level string, format string) { logPath = path - handler, _ := makeLogHandler(enabled, output, level, format) + handler := makeLogHandler(enabled, output, level, format) logInstance = slog.New(handler) } diff --git a/pkg/log/mail_logger.go b/pkg/log/mail_logger.go index 2abd7c4cb..3a5e5cd5b 100644 --- a/pkg/log/mail_logger.go +++ b/pkg/log/mail_logger.go @@ -29,7 +29,7 @@ type MailLogger struct { // NewMailLogger creates and initializes a new mail logger func NewMailLogger(configLogEnabled bool, configLogMail string, configLogMailLevel string, configLogFormat string) maillog.Logger { - handler, _ := makeLogHandler(configLogEnabled, configLogMail, configLogMailLevel, configLogFormat) + handler := makeLogHandler(configLogEnabled, configLogMail, configLogMailLevel, configLogFormat) mailLogger := &MailLogger{ logger: slog.New(handler).With("component", "mail"), diff --git a/pkg/log/watermill_logger.go b/pkg/log/watermill_logger.go index 4998023ab..b35773842 100644 --- a/pkg/log/watermill_logger.go +++ b/pkg/log/watermill_logger.go @@ -29,7 +29,7 @@ type WatermillLogger struct { // NewWatermillLogger creates and initializes a new watermill logger func NewWatermillLogger(configLogEnabled bool, configLogEvents string, configLogEventsLevel string, configLogFormat string) *WatermillLogger { - handler, _ := makeLogHandler(configLogEnabled, configLogEvents, configLogEventsLevel, configLogFormat) + handler := makeLogHandler(configLogEnabled, configLogEvents, configLogEventsLevel, configLogFormat) watermillLogger := &WatermillLogger{ logger: slog.New(handler).With("component", "events"), diff --git a/pkg/log/xorm_logger.go b/pkg/log/xorm_logger.go index e507cee36..b067d068b 100644 --- a/pkg/log/xorm_logger.go +++ b/pkg/log/xorm_logger.go @@ -31,7 +31,7 @@ type XormLogger struct { // NewXormLogger creates and initializes a new xorm logger func NewXormLogger(configLogEnabled bool, configLogDatabase string, configLogDatabaseLevel string, configLogFormat string) *XormLogger { - handler, _ := makeLogHandler(configLogEnabled, configLogDatabase, configLogDatabaseLevel, configLogFormat) + handler := makeLogHandler(configLogEnabled, configLogDatabase, configLogDatabaseLevel, configLogFormat) xormLogger := &XormLogger{ logger: slog.New(handler).With("component", "database"), diff --git a/pkg/models/api_routes.go b/pkg/models/api_routes.go index 078638ffa..ccb4ee086 100644 --- a/pkg/models/api_routes.go +++ b/pkg/models/api_routes.go @@ -18,13 +18,11 @@ package models import ( "net/http" - "reflect" - "runtime" "strings" "code.vikunja.io/api/pkg/log" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) var apiTokenRoutes = map[string]APITokenRoute{} @@ -62,42 +60,40 @@ func getRouteGroupName(path string) (finalName string, filteredParts []string) { } } -func getRouteDetail(route echo.Route) (method string, detail *RouteDetail) { - if strings.Contains(route.Name, "CreateWeb") { - return "create", &RouteDetail{ - Path: route.Path, - Method: route.Method, - } - } - if strings.Contains(route.Name, "ReadOneWeb") { - return "read_one", &RouteDetail{ - Path: route.Path, - Method: route.Method, - } - } - if strings.Contains(route.Name, "ReadAllWeb") { - return "read_all", &RouteDetail{ - Path: route.Path, - Method: route.Method, - } - } - if strings.Contains(route.Name, "UpdateWeb") { - return "update", &RouteDetail{ - Path: route.Path, - Method: route.Method, - } - } - if strings.Contains(route.Name, "DeleteWeb") { - return "delete", &RouteDetail{ - Path: route.Path, - Method: route.Method, - } - } - - return "", &RouteDetail{ +// getRouteDetail determines the API permission type from the route's HTTP method and path. +// In Echo v5, route.Name is auto-generated as METHOD:PATH, so we derive permissions from +// the HTTP method and path structure instead of the handler function name. +func getRouteDetail(route echo.RouteInfo) (method string, detail *RouteDetail) { + detail = &RouteDetail{ Path: route.Path, Method: route.Method, } + + // Check if path ends with a parameter (e.g., /:id, /:task, /:project) + pathParts := strings.Split(route.Path, "/") + lastPart := "" + if len(pathParts) > 0 { + lastPart = pathParts[len(pathParts)-1] + } + endsWithParam := strings.HasPrefix(lastPart, ":") + + switch route.Method { + case http.MethodGet: + if endsWithParam { + return "read_one", detail + } + return "read_all", detail + case http.MethodPut: + // PUT is used for creating resources in this codebase + return "create", detail + case http.MethodPost: + // POST is used for updating resources + return "update", detail + case http.MethodDelete: + return "delete", detail + } + + return "", detail } func ensureAPITokenRoutesGroup(group string) { @@ -106,27 +102,82 @@ func ensureAPITokenRoutesGroup(group string) { } } +// isStandardCRUDRoute checks if a route follows the standard CRUD pattern. +// In Echo v5, route.Name is auto-generated as METHOD:PATH, so we can no longer +// check for "(*WebHandler)" in the name. Instead, we identify CRUD routes by: +// 1. Path structure: simple /resource or /resource/:param patterns +// 2. HTTP method: GET, PUT, POST, DELETE matching CRUD semantics +// +// Standard CRUD routes have paths like: +// - /projects, /tasks, /teams, /labels, /notifications, /webhooks, /filters, etc. +// - /projects/:project, /tasks/:task, /teams/:team, etc. +// +// Non-CRUD routes have paths with additional segments or special paths like: +// - /user/settings/email, /projects/:project/background, /backgrounds/unsplash/search +func isStandardCRUDRoute(routeGroupName string, routeParts []string, _ string) bool { + // Standard CRUD resource groups that follow the WebHandler pattern + crudResources := map[string]bool{ + "projects": true, + "tasks": true, + "teams": true, + "labels": true, + "filters": true, + "notifications": true, + "webhooks": true, + "reactions": true, + "shares": true, + "buckets": true, + "views": true, + "assignees": true, + "comments": true, + "relations": true, + "attachments": true, + "projects_views": true, + "projects_teams": true, + "projects_users": true, + "projects_shares": true, + "projects_webhooks": true, + "projects_buckets": true, + "tasks_attachments": true, + "tasks_assignees": true, + "tasks_labels": true, + "tasks_comments": true, + "tasks_relations": true, + "teams_members": true, + "projects_views_tasks": true, + } + + // Check if this is a standard CRUD resource + if crudResources[routeGroupName] { + return true + } + + // Also check the base resource for nested paths + if len(routeParts) > 0 && crudResources[routeParts[0]] { + // For single-segment paths, it's CRUD if it's a known resource + if len(routeParts) == 1 { + return true + } + } + + return false +} + // CollectRoutesForAPITokenUsage gets called for every added APITokenRoute and builds a list of all routes we can use for the api tokens. -func CollectRoutesForAPITokenUsage(route echo.Route, middlewares []echo.MiddlewareFunc) { +// The requiresJWT parameter indicates if this route is protected by JWT authentication. +func CollectRoutesForAPITokenUsage(route echo.RouteInfo, requiresJWT bool) { if route.Method == "echo_route_not_found" { return } - seenJWT := false - for _, middleware := range middlewares { - if strings.Contains(runtime.FuncForPC(reflect.ValueOf(middleware).Pointer()).Name(), "github.com/labstack/echo-jwt/") { - seenJWT = true - } - } - - if !seenJWT { + if !requiresJWT { return } routeGroupName, routeParts := getRouteGroupName(route.Path) - if routeGroupName == "tokenTest" || + if routeGroupName == "token_test" || routeGroupName == "subscriptions" || routeGroupName == "tokens" || routeGroupName == "*" || @@ -134,7 +185,14 @@ func CollectRoutesForAPITokenUsage(route echo.Route, middlewares []echo.Middlewa return } - if !strings.Contains(route.Name, "(*WebHandler)") && !strings.Contains(route.Name, "Attachment") { + // Check if this is a standard CRUD route using path-based heuristics + // In Echo v5, we can no longer rely on route.Name containing "(*WebHandler)" + isCRUD := isStandardCRUDRoute(routeGroupName, routeParts, route.Method) + + // Special case for task attachments which use custom handlers + isAttachmentRoute := routeGroupName == "tasks_attachments" + + if !isCRUD && !isAttachmentRoute { routeDetail := &RouteDetail{ Path: route.Path, Method: route.Method, @@ -196,14 +254,16 @@ func CollectRoutesForAPITokenUsage(route echo.Route, middlewares []echo.Middlewa apiTokenRoutes[routeGroupName][method] = routeDetail } + // Handle task attachments specially - they use custom handlers not WebHandler if routeGroupName == "tasks_attachments" { - if strings.Contains(route.Name, "UploadTaskAttachment") { + // PUT is upload (create), GET with :attachment param is download (read_one) + if route.Method == http.MethodPut { apiTokenRoutes[routeGroupName]["create"] = &RouteDetail{ Path: route.Path, Method: route.Method, } } - if strings.Contains(route.Name, "GetTaskAttachment") { + if route.Method == http.MethodGet && strings.HasSuffix(route.Path, ":attachment") { apiTokenRoutes[routeGroupName]["read_one"] = &RouteDetail{ Path: route.Path, Method: route.Method, @@ -221,12 +281,12 @@ func CollectRoutesForAPITokenUsage(route echo.Route, middlewares []echo.Middlewa // @Security JWTKeyAuth // @Success 200 {array} models.APITokenRoute "The list of all routes." // @Router /routes [get] -func GetAvailableAPIRoutesForToken(c echo.Context) error { +func GetAvailableAPIRoutesForToken(c *echo.Context) error { return c.JSON(http.StatusOK, apiTokenRoutes) } // CanDoAPIRoute checks if a token is allowed to use the current api route -func CanDoAPIRoute(c echo.Context, token *APIToken) (can bool) { +func CanDoAPIRoute(c *echo.Context, token *APIToken) (can bool) { path := c.Path() if path == "" { // c.Path() is empty during testing, but returns the path which diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index 9eef9177f..c4841804e 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -56,7 +56,7 @@ type TaskCollection struct { // If set to `reactions`, the reactions of each task will be present in the response. // If set to `comments`, the first 50 comments of each task will be present in the response. // You can set this multiple times with different values. - Expand []TaskCollectionExpandable `query:"expand" json:"-"` + Expand []TaskCollectionExpandable `query:"expand[]" json:"-"` isSavedFilter bool diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 39d157c63..648792f1d 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -131,7 +131,7 @@ type Task struct { CommentCount *int64 `xorm:"-" json:"comment_count,omitempty"` // Behaves exactly the same as with the TaskCollection.Expand parameter - Expand []TaskCollectionExpandable `xorm:"-" json:"-" query:"expand"` + Expand []TaskCollectionExpandable `xorm:"-" json:"-" query:"expand[]"` // The position of the task - any task project can be sorted as usual by this parameter. // When accessing tasks via views with buckets, this is primarily used to sort them based on a range. diff --git a/pkg/modules/auth/auth.go b/pkg/modules/auth/auth.go index 8c2e1556c..5f6f32b94 100644 --- a/pkg/modules/auth/auth.go +++ b/pkg/modules/auth/auth.go @@ -29,7 +29,7 @@ import ( petname "github.com/dustinkirkland/golang-petname" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "xorm.io/xorm" ) @@ -46,7 +46,7 @@ type Token struct { } // NewUserAuthTokenResponse creates a new user auth token response from a user object. -func NewUserAuthTokenResponse(u *user.User, c echo.Context, long bool) error { +func NewUserAuthTokenResponse(u *user.User, c *echo.Context, long bool) error { t, err := NewUserJWTAuthtoken(u, long) if err != nil { return err @@ -104,7 +104,7 @@ func NewLinkShareJWTAuthtoken(share *models.LinkSharing) (token string, err erro } // GetAuthFromClaims returns a web.Auth object from jwt claims -func GetAuthFromClaims(c echo.Context) (a web.Auth, err error) { +func GetAuthFromClaims(c *echo.Context) (a web.Auth, err error) { // check if we have a token in context and use it if that's the case if c.Get("api_token") != nil { apiToken := c.Get("api_token").(*models.APIToken) @@ -127,7 +127,7 @@ func GetAuthFromClaims(c echo.Context) (a web.Auth, err error) { if typ == AuthTypeUser { return user.GetUserFromClaims(claims) } - return nil, echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Invalid JWT token."}) + return nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid JWT token.") } func CreateUserWithRandomUsername(s *xorm.Session, uu *user.User) (u *user.User, err error) { diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 00717f9ff..5fab9f46a 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -37,7 +37,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" petname "github.com/dustinkirkland/golang-petname" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "golang.org/x/oauth2" "xorm.io/xorm" ) @@ -128,7 +128,7 @@ func (p *Provider) Issuer() (issuerURL string, err error) { // @Success 200 {object} auth.Token // @Failure 500 {object} models.Message "Internal error" // @Router /auth/openid/{provider}/callback [post] -func HandleCallback(c echo.Context) error { +func HandleCallback(c *echo.Context) error { provider, oauthToken, idToken, err := getProviderAndOidcTokens(c) if err != nil { @@ -419,7 +419,7 @@ func getClaims(provider *Provider, oauth2Token *oauth2.Token, idToken *oidc.IDTo return cl, nil } -func getProviderAndOidcTokens(c echo.Context) (*Provider, *oauth2.Token, *oidc.IDToken, error) { +func getProviderAndOidcTokens(c *echo.Context) (*Provider, *oauth2.Token, *oidc.IDToken, error) { cb := &Callback{} if err := c.Bind(cb); err != nil { diff --git a/pkg/modules/background/handler/background.go b/pkg/modules/background/handler/background.go index dd8a2fa7b..1308c3d2e 100644 --- a/pkg/modules/background/handler/background.go +++ b/pkg/modules/background/handler/background.go @@ -46,7 +46,7 @@ import ( "github.com/bbrks/go-blurhash" "github.com/gabriel-vasile/mimetype" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "golang.org/x/image/draw" "xorm.io/xorm" ) @@ -67,12 +67,12 @@ type BackgroundProvider struct { } // SearchBackgrounds is the web handler to search for backgrounds -func (bp *BackgroundProvider) SearchBackgrounds(c echo.Context) error { +func (bp *BackgroundProvider) SearchBackgrounds(c *echo.Context) error { p := bp.Provider() err := c.Bind(p) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided: "+err.Error()).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided: "+err.Error()).Wrap(err) } search := c.QueryParam("s") @@ -81,7 +81,7 @@ func (bp *BackgroundProvider) SearchBackgrounds(c echo.Context) error { if pg != "" { page, err = strconv.ParseInt(pg, 10, 64) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid page number: "+err.Error()).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "Invalid page number: "+err.Error()).Wrap(err) } } @@ -91,27 +91,27 @@ func (bp *BackgroundProvider) SearchBackgrounds(c echo.Context) error { result, err := p.Search(s, search, page) if err != nil { _ = s.Rollback() - return echo.NewHTTPError(http.StatusBadRequest, "An error occurred: "+err.Error()).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "An error occurred: "+err.Error()).Wrap(err) } if err := s.Commit(); err != nil { _ = s.Rollback() - return echo.NewHTTPError(http.StatusBadRequest, "An error occurred: "+err.Error()).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "An error occurred: "+err.Error()).Wrap(err) } return c.JSON(http.StatusOK, result) } // This function does all kinds of preparations for setting and uploading a background -func (bp *BackgroundProvider) setBackgroundPreparations(s *xorm.Session, c echo.Context) (project *models.Project, auth web.Auth, err error) { +func (bp *BackgroundProvider) setBackgroundPreparations(s *xorm.Session, c *echo.Context) (project *models.Project, auth web.Auth, err error) { auth, err = auth2.GetAuthFromClaims(c) if err != nil { - return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error()).SetInternal(err) + return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error()).Wrap(err) } projectID, err := strconv.ParseInt(c.Param("project"), 10, 64) if err != nil { - return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid project ID: "+err.Error()).SetInternal(err) + return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid project ID: "+err.Error()).Wrap(err) } // Check if the user has the permission to change the project background @@ -130,7 +130,7 @@ func (bp *BackgroundProvider) setBackgroundPreparations(s *xorm.Session, c echo. } // SetBackground sets an Image as project background -func (bp *BackgroundProvider) SetBackground(c echo.Context) error { +func (bp *BackgroundProvider) SetBackground(c *echo.Context) error { s := db.NewSession() defer s.Close() @@ -146,7 +146,7 @@ func (bp *BackgroundProvider) SetBackground(c echo.Context) error { err = c.Bind(image) if err != nil { _ = s.Rollback() - return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided: "+err.Error()).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided: "+err.Error()).Wrap(err) } err = p.Set(s, image, project, auth) @@ -177,7 +177,7 @@ func CreateBlurHash(srcf io.Reader) (hash string, err error) { } // UploadBackground uploads a background and passes the id of the uploaded file as an Image to the Set function of the BackgroundProvider. -func (bp *BackgroundProvider) UploadBackground(c echo.Context) error { +func (bp *BackgroundProvider) UploadBackground(c *echo.Context) error { s := db.NewSession() defer s.Close() @@ -297,15 +297,15 @@ func SaveBackgroundFile(s *xorm.Session, auth web.Auth, project *models.Project, return err } -func checkProjectBackgroundRights(s *xorm.Session, c echo.Context) (project *models.Project, auth web.Auth, err error) { +func checkProjectBackgroundRights(s *xorm.Session, c *echo.Context) (project *models.Project, auth web.Auth, err error) { auth, err = auth2.GetAuthFromClaims(c) if err != nil { - return nil, auth, echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error()).SetInternal(err) + return nil, auth, echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error()).Wrap(err) } projectID, err := strconv.ParseInt(c.Param("project"), 10, 64) if err != nil { - return nil, auth, echo.NewHTTPError(http.StatusBadRequest, "Invalid project ID: "+err.Error()).SetInternal(err) + return nil, auth, echo.NewHTTPError(http.StatusBadRequest, "Invalid project ID: "+err.Error()).Wrap(err) } // Check if a background for this project exists + Permissions @@ -318,7 +318,7 @@ func checkProjectBackgroundRights(s *xorm.Session, c echo.Context) (project *mod if !can { _ = s.Rollback() log.Infof("Tried to get project background of project %d while not having the permissions for it (User: %v)", projectID, auth) - return nil, auth, echo.NewHTTPError(http.StatusForbidden) + return nil, auth, echo.NewHTTPError(http.StatusForbidden, "Forbidden") } return @@ -337,7 +337,7 @@ func checkProjectBackgroundRights(s *xorm.Session, c echo.Context) (project *mod // @Failure 404 {object} models.Message "The project does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /projects/{id}/background [get] -func GetProjectBackground(c echo.Context) error { +func GetProjectBackground(c *echo.Context) error { s := db.NewSession() defer s.Close() @@ -349,7 +349,7 @@ func GetProjectBackground(c echo.Context) error { if project.BackgroundFileID == 0 { _ = s.Rollback() - return echo.NotFoundHandler(c) + return echo.NewHTTPError(http.StatusNotFound, "Project background not found") } // Get the file @@ -397,7 +397,7 @@ func GetProjectBackground(c echo.Context) error { // @Failure 404 {object} models.Message "The project does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /projects/{id}/background [delete] -func RemoveProjectBackground(c echo.Context) error { +func RemoveProjectBackground(c *echo.Context) error { s := db.NewSession() defer s.Close() diff --git a/pkg/modules/background/unsplash/proxy.go b/pkg/modules/background/unsplash/proxy.go index d5f099c51..b497d82a7 100644 --- a/pkg/modules/background/unsplash/proxy.go +++ b/pkg/modules/background/unsplash/proxy.go @@ -21,10 +21,10 @@ import ( "net/http" "strings" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) -func unsplashImage(url string, c echo.Context) error { +func unsplashImage(url string, c *echo.Context) error { // Replacing and appending the url for security reasons req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://images.unsplash.com/"+strings.Replace(url, "https://images.unsplash.com/", "", 1), nil) if err != nil { @@ -52,7 +52,7 @@ func unsplashImage(url string, c echo.Context) error { // @Failure 404 {object} models.Message "The image does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /backgrounds/unsplash/image/{image} [get] -func ProxyUnsplashImage(c echo.Context) error { +func ProxyUnsplashImage(c *echo.Context) error { photo, err := getUnsplashPhotoInfoByID(c.Param("image")) if err != nil { return err @@ -72,7 +72,7 @@ func ProxyUnsplashImage(c echo.Context) error { // @Failure 404 {object} models.Message "The image does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /backgrounds/unsplash/image/{image}/thumb [get] -func ProxyUnsplashThumb(c echo.Context) error { +func ProxyUnsplashThumb(c *echo.Context) error { photo, err := getUnsplashPhotoInfoByID(c.Param("image")) if err != nil { return err diff --git a/pkg/modules/migration/handler/common.go b/pkg/modules/migration/handler/common.go index 2fefe7ece..8917d4a21 100644 --- a/pkg/modules/migration/handler/common.go +++ b/pkg/modules/migration/handler/common.go @@ -21,10 +21,10 @@ import ( "code.vikunja.io/api/pkg/modules/migration" user2 "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) -func status(ms migration.MigratorName, c echo.Context) error { +func status(ms migration.MigratorName, c *echo.Context) error { user, err := user2.GetCurrentUser(c) if err != nil { return err diff --git a/pkg/modules/migration/handler/handler.go b/pkg/modules/migration/handler/handler.go index c71be491c..840dbacd6 100644 --- a/pkg/modules/migration/handler/handler.go +++ b/pkg/modules/migration/handler/handler.go @@ -23,7 +23,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/migration" user2 "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) var registeredMigrators map[string]*MigrationWeb @@ -52,13 +52,13 @@ func (mw *MigrationWeb) RegisterMigrator(g *echo.Group) { } // AuthURL is the web handler to get the auth url -func (mw *MigrationWeb) AuthURL(c echo.Context) error { +func (mw *MigrationWeb) AuthURL(c *echo.Context) error { ms := mw.MigrationStruct() return c.JSON(http.StatusOK, &AuthURL{URL: ms.AuthURL()}) } // Migrate calls the migration method -func (mw *MigrationWeb) Migrate(c echo.Context) error { +func (mw *MigrationWeb) Migrate(c *echo.Context) error { ms := mw.MigrationStruct() // Get the user from context @@ -82,7 +82,7 @@ func (mw *MigrationWeb) Migrate(c echo.Context) error { // Bind user request stuff err = c.Bind(ms) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided: "+err.Error()).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided: "+err.Error()).Wrap(err) } err = events.Dispatch(&MigrationRequestedEvent{ @@ -98,7 +98,7 @@ func (mw *MigrationWeb) Migrate(c echo.Context) error { } // Status returns whether or not a user has already done this migration -func (mw *MigrationWeb) Status(c echo.Context) error { +func (mw *MigrationWeb) Status(c *echo.Context) error { ms := mw.MigrationStruct() return status(ms, c) diff --git a/pkg/modules/migration/handler/handler_file.go b/pkg/modules/migration/handler/handler_file.go index 97138494f..8fae1d775 100644 --- a/pkg/modules/migration/handler/handler_file.go +++ b/pkg/modules/migration/handler/handler_file.go @@ -22,7 +22,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/migration" user2 "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type FileMigratorWeb struct { @@ -37,7 +37,7 @@ func (fw *FileMigratorWeb) RegisterRoutes(g *echo.Group) { } // Migrate calls the migration method -func (fw *FileMigratorWeb) Migrate(c echo.Context) error { +func (fw *FileMigratorWeb) Migrate(c *echo.Context) error { ms := fw.MigrationStruct() // Get the user from context @@ -76,7 +76,7 @@ func (fw *FileMigratorWeb) Migrate(c echo.Context) error { } // Status returns whether or not a user has already done this migration -func (fw *FileMigratorWeb) Status(c echo.Context) error { +func (fw *FileMigratorWeb) Status(c *echo.Context) error { ms := fw.MigrationStruct() return status(ms, c) diff --git a/pkg/plugins/interfaces.go b/pkg/plugins/interfaces.go index aef6cc8f7..b517b97ba 100644 --- a/pkg/plugins/interfaces.go +++ b/pkg/plugins/interfaces.go @@ -17,7 +17,7 @@ package plugins import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "src.techknowlogick.com/xormigrate" ) diff --git a/pkg/plugins/manager.go b/pkg/plugins/manager.go index 50cde6a73..a704ccb7a 100644 --- a/pkg/plugins/manager.go +++ b/pkg/plugins/manager.go @@ -26,7 +26,7 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/migration" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // Manager handles loading and managing plugins. diff --git a/pkg/routes/api/v1/avatar.go b/pkg/routes/api/v1/avatar.go index 0f666ff0b..5efea2f64 100644 --- a/pkg/routes/api/v1/avatar.go +++ b/pkg/routes/api/v1/avatar.go @@ -32,7 +32,7 @@ import ( "strings" "github.com/gabriel-vasile/mimetype" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // GetAvatar returns a user's avatar @@ -46,7 +46,7 @@ import ( // @Failure 404 {object} models.Message "The user does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /{username}/avatar [get] -func GetAvatar(c echo.Context) error { +func GetAvatar(c *echo.Context) error { // Get the username username := c.Param("username") @@ -104,7 +104,7 @@ func GetAvatar(c echo.Context) error { // @Failure 403 {object} models.Message "File too large." // @Failure 500 {object} models.Message "Internal error" // @Router /user/settings/avatar/upload [put] -func UploadAvatar(c echo.Context) (err error) { +func UploadAvatar(c *echo.Context) (err error) { s := db.NewSession() defer s.Close() diff --git a/pkg/routes/api/v1/docs.go b/pkg/routes/api/v1/docs.go index 3dd46f89f..b1ce14567 100644 --- a/pkg/routes/api/v1/docs.go +++ b/pkg/routes/api/v1/docs.go @@ -24,24 +24,24 @@ import ( "code.vikunja.io/api/pkg/log" _ "code.vikunja.io/api/pkg/swagger" // To make sure the swag files are properly registered - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/swaggo/swag" ) // DocsJSON serves swagger doc json specs -func DocsJSON(c echo.Context) error { +func DocsJSON(c *echo.Context) error { doc, err := swag.ReadDoc() if err != nil { log.Error(err.Error()) - return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error").Wrap(err) } return c.Blob(http.StatusOK, echo.MIMEApplicationJSON, []byte(doc)) } // RedocUI serves everything needed to provide the redoc ui -func RedocUI(c echo.Context) error { +func RedocUI(c *echo.Context) error { return c.HTML(http.StatusOK, RedocUITemplate) } diff --git a/pkg/routes/api/v1/info.go b/pkg/routes/api/v1/info.go index 3bd8f4956..9a5ef9946 100644 --- a/pkg/routes/api/v1/info.go +++ b/pkg/routes/api/v1/info.go @@ -29,7 +29,7 @@ import ( vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file" "code.vikunja.io/api/pkg/version" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type vikunjaInfos struct { @@ -86,7 +86,7 @@ type legalInfo struct { // @Produce json // @Success 200 {object} v1.vikunjaInfos // @Router /info [get] -func Info(c echo.Context) error { +func Info(c *echo.Context) error { info := vikunjaInfos{ Version: version.Version, FrontendURL: config.ServicePublicURL.GetString(), diff --git a/pkg/routes/api/v1/link_sharing_auth.go b/pkg/routes/api/v1/link_sharing_auth.go index e9c4b0992..9e20a94f8 100644 --- a/pkg/routes/api/v1/link_sharing_auth.go +++ b/pkg/routes/api/v1/link_sharing_auth.go @@ -23,7 +23,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // LinkShareToken represents a link share auth token with extra infos about the actual link share @@ -51,7 +51,7 @@ type LinkShareAuth struct { // @Failure 400 {object} web.HTTPError "Invalid link share object provided." // @Failure 500 {object} models.Message "Internal error" // @Router /shares/{share}/auth [post] -func AuthenticateLinkShare(c echo.Context) error { +func AuthenticateLinkShare(c *echo.Context) error { sh := &LinkShareAuth{} err := c.Bind(sh) if err != nil { diff --git a/pkg/routes/api/v1/login.go b/pkg/routes/api/v1/login.go index 44802c0c5..eb5655eca 100644 --- a/pkg/routes/api/v1/login.go +++ b/pkg/routes/api/v1/login.go @@ -28,7 +28,7 @@ import ( user2 "code.vikunja.io/api/pkg/user" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // Login is the login handler @@ -43,7 +43,7 @@ import ( // @Failure 412 {object} models.Message "Invalid totp passcode." // @Failure 403 {object} models.Message "Invalid username or password." // @Router /login [post] -func Login(c echo.Context) (err error) { +func Login(c *echo.Context) (err error) { u := user2.Login{} if err := c.Bind(&u); err != nil { return c.JSON(http.StatusBadRequest, models.Message{Message: "Please provide a username and password."}) @@ -126,7 +126,7 @@ func Login(c echo.Context) (err error) { // @Success 200 {object} auth.Token // @Failure 400 {object} models.Message "Only user token are available for renew." // @Router /user/token [post] -func RenewToken(c echo.Context) (err error) { +func RenewToken(c *echo.Context) (err error) { s := db.NewSession() defer s.Close() diff --git a/pkg/routes/api/v1/notifications.go b/pkg/routes/api/v1/notifications.go index 3a3e87304..a44f81ea4 100644 --- a/pkg/routes/api/v1/notifications.go +++ b/pkg/routes/api/v1/notifications.go @@ -23,7 +23,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/api/pkg/notifications" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // MarkAllNotificationsAsRead marks all notifications of a user as read @@ -34,7 +34,7 @@ import ( // @Success 200 {object} models.Message "All notifications marked as read." // @Failure 500 {object} models.Message "Internal error" // @Router /notifications [post] -func MarkAllNotificationsAsRead(c echo.Context) error { +func MarkAllNotificationsAsRead(c *echo.Context) error { s := db.NewSession() defer s.Close() diff --git a/pkg/routes/api/v1/task_attachment.go b/pkg/routes/api/v1/task_attachment.go index 79d0fb797..e23781503 100644 --- a/pkg/routes/api/v1/task_attachment.go +++ b/pkg/routes/api/v1/task_attachment.go @@ -29,7 +29,7 @@ import ( auth2 "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/api/pkg/web" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // attachmentUploadError represents a structured error for attachment upload failures @@ -68,11 +68,11 @@ func toAttachmentUploadError(err error) attachmentUploadError { // @Failure 404 {object} models.Message "The task does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /tasks/{id}/attachments [put] -func UploadTaskAttachment(c echo.Context) error { +func UploadTaskAttachment(c *echo.Context) error { var taskAttachment models.TaskAttachment if err := c.Bind(&taskAttachment); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No task ID provided").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No task ID provided").Wrap(err) } // Permissions check @@ -98,7 +98,7 @@ func UploadTaskAttachment(c echo.Context) error { if err != nil { _ = s.Rollback() if errors.Is(err, http.ErrNotMultipart) { - return echo.NewHTTPError(http.StatusBadRequest, "No multipart form provided") + return echo.NewHTTPError(http.StatusBadRequest, "No multipart form provided").Wrap(err) } return err } @@ -152,11 +152,11 @@ func UploadTaskAttachment(c echo.Context) error { // @Failure 404 {object} models.Message "The task does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /tasks/{id}/attachments/{attachmentID} [get] -func GetTaskAttachment(c echo.Context) error { +func GetTaskAttachment(c *echo.Context) error { var taskAttachment models.TaskAttachment if err := c.Bind(&taskAttachment); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No task ID provided").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No task ID provided").Wrap(err) } // Permissions check @@ -213,7 +213,7 @@ func GetTaskAttachment(c echo.Context) error { c.Response().Header().Set("Last-Modified", taskAttachment.File.Created.UTC().Format(http.TimeFormat)) // Stream the file content directly to the response - _, err = io.Copy(c.Response().Writer, taskAttachment.File.File) + _, err = io.Copy(c.Response(), taskAttachment.File.File) if err != nil { return err } diff --git a/pkg/routes/api/v1/testing.go b/pkg/routes/api/v1/testing.go index be695cd85..6f32d2395 100644 --- a/pkg/routes/api/v1/testing.go +++ b/pkg/routes/api/v1/testing.go @@ -25,7 +25,7 @@ import ( "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/log" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // HandleTesting is the web handler to reset the db @@ -38,7 +38,7 @@ import ( // @Success 201 {array} user.User "Everything has been imported successfully." // @Failure 500 {object} models.Message "Internal server error." // @Router /test/{table} [patch] -func HandleTesting(c echo.Context) error { +func HandleTesting(c *echo.Context) error { token := c.Request().Header.Get("Authorization") if token != config.ServiceTestingtoken.GetString() { return echo.ErrForbidden diff --git a/pkg/routes/api/v1/token_check.go b/pkg/routes/api/v1/token_check.go index e2c9812a9..cd4d3492d 100644 --- a/pkg/routes/api/v1/token_check.go +++ b/pkg/routes/api/v1/token_check.go @@ -22,11 +22,11 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // CheckToken checks prints a message if the token is valid or not. Currently only used for testing purposes. -func CheckToken(c echo.Context) error { +func CheckToken(c *echo.Context) error { user := c.Get("user").(*jwt.Token) @@ -36,6 +36,6 @@ func CheckToken(c echo.Context) error { } // TestToken returns a simple test message. Used for testing purposes. -func TestToken(c echo.Context) error { +func TestToken(c *echo.Context) error { return c.JSON(http.StatusOK, models.Message{Message: "ok"}) } diff --git a/pkg/routes/api/v1/user_caldav_token.go b/pkg/routes/api/v1/user_caldav_token.go index 5ebccee63..4e822c829 100644 --- a/pkg/routes/api/v1/user_caldav_token.go +++ b/pkg/routes/api/v1/user_caldav_token.go @@ -23,7 +23,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // GenerateCaldavToken is the handler to create a caldav token @@ -38,7 +38,7 @@ import ( // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/token/caldav [put] -func GenerateCaldavToken(c echo.Context) (err error) { +func GenerateCaldavToken(c *echo.Context) (err error) { u, err := user.GetCurrentUser(c) if err != nil { @@ -65,7 +65,7 @@ func GenerateCaldavToken(c echo.Context) (err error) { // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/token/caldav [get] -func GetCaldavTokens(c echo.Context) error { +func GetCaldavTokens(c *echo.Context) error { u, err := user.GetCurrentUser(c) if err != nil { return err @@ -91,7 +91,7 @@ func GetCaldavTokens(c echo.Context) error { // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/token/caldav/{id} [delete] -func DeleteCaldavToken(c echo.Context) error { +func DeleteCaldavToken(c *echo.Context) error { u, err := user.GetCurrentUser(c) if err != nil { return err diff --git a/pkg/routes/api/v1/user_confirm_email.go b/pkg/routes/api/v1/user_confirm_email.go index 259898886..e01865103 100644 --- a/pkg/routes/api/v1/user_confirm_email.go +++ b/pkg/routes/api/v1/user_confirm_email.go @@ -23,7 +23,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // UserConfirmEmail is the handler to confirm a user email @@ -37,11 +37,11 @@ import ( // @Failure 412 {object} web.HTTPError "Bad token provided." // @Failure 500 {object} models.Message "Internal error" // @Router /user/confirm [post] -func UserConfirmEmail(c echo.Context) error { +func UserConfirmEmail(c *echo.Context) error { // Check for Request Content var emailConfirm user.EmailConfirm if err := c.Bind(&emailConfirm); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No token provided.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No token provided.").Wrap(err) } s := db.NewSession() diff --git a/pkg/routes/api/v1/user_deletion.go b/pkg/routes/api/v1/user_deletion.go index 4101ad8a4..8e3cf869b 100644 --- a/pkg/routes/api/v1/user_deletion.go +++ b/pkg/routes/api/v1/user_deletion.go @@ -23,7 +23,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type UserPasswordConfirmation struct { @@ -45,7 +45,7 @@ type UserDeletionRequestConfirm struct { // @Failure 412 {object} web.HTTPError "Bad password provided." // @Failure 500 {object} models.Message "Internal error" // @Router /user/deletion/request [post] -func UserRequestDeletion(c echo.Context) error { +func UserRequestDeletion(c *echo.Context) error { s := db.NewSession() defer s.Close() @@ -64,12 +64,12 @@ func UserRequestDeletion(c echo.Context) error { if u.IsLocalUser() { var deletionRequest UserPasswordConfirmation if err := c.Bind(&deletionRequest); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No password provided.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No password provided.").Wrap(err) } err = c.Validate(deletionRequest) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, err.Error()).Wrap(err) } err = user.CheckUserPassword(u, deletionRequest.Password) @@ -105,15 +105,15 @@ func UserRequestDeletion(c echo.Context) error { // @Failure 412 {object} web.HTTPError "Bad token provided." // @Failure 500 {object} models.Message "Internal error" // @Router /user/deletion/confirm [post] -func UserConfirmDeletion(c echo.Context) error { +func UserConfirmDeletion(c *echo.Context) error { var deleteConfirmation UserDeletionRequestConfirm if err := c.Bind(&deleteConfirmation); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No token provided.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No token provided.").Wrap(err) } err := c.Validate(deleteConfirmation) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, err.Error()).Wrap(err) } s := db.NewSession() @@ -156,7 +156,7 @@ func UserConfirmDeletion(c echo.Context) error { // @Failure 412 {object} web.HTTPError "Bad password provided." // @Failure 500 {object} models.Message "Internal error" // @Router /user/deletion/cancel [post] -func UserCancelDeletion(c echo.Context) error { +func UserCancelDeletion(c *echo.Context) error { s := db.NewSession() defer s.Close() @@ -175,12 +175,12 @@ func UserCancelDeletion(c echo.Context) error { if u.IsLocalUser() { var deletionRequest UserPasswordConfirmation if err := c.Bind(&deletionRequest); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No password provided.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No password provided.").Wrap(err) } err = c.Validate(deletionRequest) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, err.Error()).Wrap(err) } err = user.CheckUserPassword(u, deletionRequest.Password) diff --git a/pkg/routes/api/v1/user_export.go b/pkg/routes/api/v1/user_export.go index cd8858f44..134230867 100644 --- a/pkg/routes/api/v1/user_export.go +++ b/pkg/routes/api/v1/user_export.go @@ -26,11 +26,11 @@ import ( "code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "xorm.io/xorm" ) -func checkExportRequest(c echo.Context) (s *xorm.Session, u *user.User, err error) { +func checkExportRequest(c *echo.Context) (s *xorm.Session, u *user.User, err error) { s = db.NewSession() defer s.Close() @@ -52,12 +52,12 @@ func checkExportRequest(c echo.Context) (s *xorm.Session, u *user.User, err erro var pass UserPasswordConfirmation if err := c.Bind(&pass); err != nil { - return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "No password provided.").SetInternal(err) + return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "No password provided.").Wrap(err) } err = c.Validate(pass) if err != nil { - return nil, nil, echo.NewHTTPError(http.StatusBadRequest, err).SetInternal(err) + return nil, nil, echo.NewHTTPError(http.StatusBadRequest, err.Error()).Wrap(err) } err = user.CheckUserPassword(u, pass.Password) @@ -80,7 +80,7 @@ func checkExportRequest(c echo.Context) (s *xorm.Session, u *user.User, err erro // @Failure 400 {object} web.HTTPError "Something's invalid." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/export/request [post] -func RequestUserDataExport(c echo.Context) error { +func RequestUserDataExport(c *echo.Context) error { s, u, err := checkExportRequest(c) if err != nil { return err @@ -115,7 +115,7 @@ func RequestUserDataExport(c echo.Context) error { // @Failure 404 {object} web.HTTPError "No user data export found." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/export/download [post] -func DownloadUserDataExport(c echo.Context) error { +func DownloadUserDataExport(c *echo.Context) error { s, u, err := checkExportRequest(c) if err != nil { return err @@ -168,7 +168,7 @@ type UserExportStatus struct { // @Security JWTKeyAuth // @Success 200 {object} v1.UserExportStatus // @Router /user/export [get] -func GetUserExportStatus(c echo.Context) error { +func GetUserExportStatus(c *echo.Context) error { s := db.NewSession() defer s.Close() diff --git a/pkg/routes/api/v1/user_list.go b/pkg/routes/api/v1/user_list.go index df401fece..8494b5c8a 100644 --- a/pkg/routes/api/v1/user_list.go +++ b/pkg/routes/api/v1/user_list.go @@ -25,7 +25,7 @@ import ( "code.vikunja.io/api/pkg/models" auth2 "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // UserList gets all information about a list of users @@ -40,7 +40,7 @@ import ( // @Failure 400 {object} web.HTTPError "Something's invalid." // @Failure 500 {object} models.Message "Internal server error." // @Router /users [get] -func UserList(c echo.Context) error { +func UserList(c *echo.Context) error { search := c.QueryParam("s") s := db.NewSession() @@ -80,7 +80,7 @@ func UserList(c echo.Context) error { // @Failure 401 {object} web.HTTPError "The user does not have the permission to see the project." // @Failure 500 {object} models.Message "Internal server error." // @Router /projects/{id}/projectusers [get] -func ListUsersForProject(c echo.Context) error { +func ListUsersForProject(c *echo.Context) error { projectID, err := strconv.ParseInt(c.Param("project"), 10, 64) if err != nil { return c.JSON(http.StatusBadRequest, map[string]interface{}{ diff --git a/pkg/routes/api/v1/user_password_reset.go b/pkg/routes/api/v1/user_password_reset.go index 9c6f2dfdf..f448f53f1 100644 --- a/pkg/routes/api/v1/user_password_reset.go +++ b/pkg/routes/api/v1/user_password_reset.go @@ -23,7 +23,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // UserResetPassword is the handler to change a users password @@ -37,11 +37,11 @@ import ( // @Failure 400 {object} web.HTTPError "Bad token provided." // @Failure 500 {object} models.Message "Internal error" // @Router /user/password/reset [post] -func UserResetPassword(c echo.Context) error { +func UserResetPassword(c *echo.Context) error { // Check for Request Content var pwReset user.PasswordReset if err := c.Bind(&pwReset); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No password provided.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No password provided.").Wrap(err) } s := db.NewSession() @@ -72,15 +72,15 @@ func UserResetPassword(c echo.Context) error { // @Failure 404 {object} web.HTTPError "The user does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /user/password/token [post] -func UserRequestResetPasswordToken(c echo.Context) error { +func UserRequestResetPasswordToken(c *echo.Context) error { // Check for Request Content var pwTokenReset user.PasswordTokenRequest if err := c.Bind(&pwTokenReset); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No username provided.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No username provided.").Wrap(err) } if err := c.Validate(pwTokenReset); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, err.Error()).Wrap(err) } s := db.NewSession() diff --git a/pkg/routes/api/v1/user_register.go b/pkg/routes/api/v1/user_register.go index 71b06f5ad..f358811cf 100644 --- a/pkg/routes/api/v1/user_register.go +++ b/pkg/routes/api/v1/user_register.go @@ -25,7 +25,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type UserRegister struct { @@ -45,7 +45,7 @@ type UserRegister struct { // @Failure 400 {object} web.HTTPError "No or invalid user register object provided / User already exists." // @Failure 500 {object} models.Message "Internal error" // @Router /register [post] -func RegisterUser(c echo.Context) error { +func RegisterUser(c *echo.Context) error { if !config.ServiceEnableRegistration.GetBool() { return echo.ErrNotFound } diff --git a/pkg/routes/api/v1/user_settings.go b/pkg/routes/api/v1/user_settings.go index 59f970fba..afb700065 100644 --- a/pkg/routes/api/v1/user_settings.go +++ b/pkg/routes/api/v1/user_settings.go @@ -21,7 +21,7 @@ import ( "fmt" "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/tkuchiki/go-timezone" "code.vikunja.io/api/pkg/db" @@ -76,7 +76,7 @@ type UserSettings struct { // @Failure 400 {object} web.HTTPError "Something's invalid." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/avatar [get] -func GetUserAvatarProvider(c echo.Context) error { +func GetUserAvatarProvider(c *echo.Context) error { u, err := user2.GetCurrentUser(c) if err != nil { @@ -113,12 +113,12 @@ func GetUserAvatarProvider(c echo.Context) error { // @Failure 400 {object} web.HTTPError "Something's invalid." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/avatar [post] -func ChangeUserAvatarProvider(c echo.Context) error { +func ChangeUserAvatarProvider(c *echo.Context) error { uap := &UserAvatarProvider{} err := c.Bind(uap) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Bad avatar type provided.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "Bad avatar type provided.").Wrap(err) } u, err := user2.GetCurrentUser(c) @@ -172,7 +172,7 @@ func ChangeUserAvatarProvider(c echo.Context) error { // @Failure 400 {object} web.HTTPError "Something's invalid." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/general [post] -func UpdateGeneralUserSettings(c echo.Context) error { +func UpdateGeneralUserSettings(c *echo.Context) error { us := &UserSettings{} err := c.Bind(us) if err != nil { @@ -185,7 +185,7 @@ func UpdateGeneralUserSettings(c echo.Context) error { err = c.Validate(us) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err).SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, err.Error()).Wrap(err) } u, err := user2.GetCurrentUser(c) @@ -244,7 +244,7 @@ func UpdateGeneralUserSettings(c echo.Context) error { // @Success 200 {array} string "All available time zones." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/timezones [get] -func GetAvailableTimezones(c echo.Context) error { +func GetAvailableTimezones(c *echo.Context) error { allTimezones := timezone.New().Timezones() timezoneMap := make(map[string]bool) // to filter all duplicates diff --git a/pkg/routes/api/v1/user_show.go b/pkg/routes/api/v1/user_show.go index 465d9e7e2..9e850f1d6 100644 --- a/pkg/routes/api/v1/user_show.go +++ b/pkg/routes/api/v1/user_show.go @@ -29,7 +29,7 @@ import ( "code.vikunja.io/api/pkg/db" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type UserWithSettings struct { @@ -51,10 +51,10 @@ type UserWithSettings struct { // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." // @Router /user [get] -func UserShow(c echo.Context) error { +func UserShow(c *echo.Context) error { a, err := auth.GetAuthFromClaims(c) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Error getting current user.").SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Error getting current user.").Wrap(err) } s := db.NewSession() diff --git a/pkg/routes/api/v1/user_totp.go b/pkg/routes/api/v1/user_totp.go index 003981394..4434eaee5 100644 --- a/pkg/routes/api/v1/user_totp.go +++ b/pkg/routes/api/v1/user_totp.go @@ -28,12 +28,12 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "xorm.io/xorm" ) // getLocalUserFromContext is a helper function to get the current local user and database session -func getLocalUserFromContext(c echo.Context) (*user.User, *xorm.Session, error) { +func getLocalUserFromContext(c *echo.Context) (*user.User, *xorm.Session, error) { s := db.NewSession() u, err := user.GetCurrentUserFromDB(s, c) @@ -62,7 +62,7 @@ func getLocalUserFromContext(c echo.Context) (*user.User, *xorm.Session, error) // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/totp/enroll [post] -func UserTOTPEnroll(c echo.Context) error { +func UserTOTPEnroll(c *echo.Context) error { u, s, err := getLocalUserFromContext(c) if err != nil { return err @@ -97,7 +97,7 @@ func UserTOTPEnroll(c echo.Context) error { // @Failure 412 {object} web.HTTPError "TOTP is not enrolled." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/totp/enable [post] -func UserTOTPEnable(c echo.Context) error { +func UserTOTPEnable(c *echo.Context) error { u, s, err := getLocalUserFromContext(c) if err != nil { return err @@ -143,7 +143,7 @@ func UserTOTPEnable(c echo.Context) error { // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/totp/disable [post] -func UserTOTPDisable(c echo.Context) error { +func UserTOTPDisable(c *echo.Context) error { login := &user.Login{} if err := c.Bind(login); err != nil { log.Debugf("Invalid model error. Internal error was: %s", err.Error()) @@ -190,7 +190,7 @@ func UserTOTPDisable(c echo.Context) error { // @Success 200 {file} blob "The qr code as jpeg image" // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/totp/qrcode [get] -func UserTOTPQrCode(c echo.Context) error { +func UserTOTPQrCode(c *echo.Context) error { u, s, err := getLocalUserFromContext(c) if err != nil { return err @@ -228,7 +228,7 @@ func UserTOTPQrCode(c echo.Context) error { // @Success 200 {object} user.TOTP "The totp settings." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/totp [get] -func UserTOTP(c echo.Context) error { +func UserTOTP(c *echo.Context) error { u, s, err := getLocalUserFromContext(c) if err != nil { return err diff --git a/pkg/routes/api/v1/user_update_email.go b/pkg/routes/api/v1/user_update_email.go index 1841ad7fd..ea1077075 100644 --- a/pkg/routes/api/v1/user_update_email.go +++ b/pkg/routes/api/v1/user_update_email.go @@ -26,7 +26,7 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // UpdateUserEmail is the handler to let a user update their email address. @@ -42,7 +42,7 @@ import ( // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/settings/email [post] -func UpdateUserEmail(c echo.Context) (err error) { +func UpdateUserEmail(c *echo.Context) (err error) { var emailUpdate = &user.EmailUpdate{} if err := c.Bind(emailUpdate); err != nil { diff --git a/pkg/routes/api/v1/user_update_password.go b/pkg/routes/api/v1/user_update_password.go index 5e5b6fd83..8ccedc503 100644 --- a/pkg/routes/api/v1/user_update_password.go +++ b/pkg/routes/api/v1/user_update_password.go @@ -23,7 +23,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // UserPassword holds a user password. Used to update it. @@ -45,17 +45,17 @@ type UserPassword struct { // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." // @Router /user/password [post] -func UserChangePassword(c echo.Context) error { +func UserChangePassword(c *echo.Context) error { // Check if the user is itself doer, err := user.GetCurrentUser(c) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Error getting current user.").SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Error getting current user.").Wrap(err) } // Check for Request Content var newPW UserPassword if err := c.Bind(&newPW); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "No password provided.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "No password provided.").Wrap(err) } if newPW.OldPassword == "" { diff --git a/pkg/routes/api/v1/webhooks.go b/pkg/routes/api/v1/webhooks.go index 5736ca3d3..42fb399a9 100644 --- a/pkg/routes/api/v1/webhooks.go +++ b/pkg/routes/api/v1/webhooks.go @@ -20,7 +20,7 @@ import ( "net/http" "code.vikunja.io/api/pkg/models" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // GetAvailableWebhookEvents returns a list of all possible webhook target events @@ -33,6 +33,6 @@ import ( // @Success 200 {array} string "The list of all possible webhook events" // @Failure 500 {object} models.Message "Internal server error" // @Router /webhooks/events [get] -func GetAvailableWebhookEvents(c echo.Context) error { +func GetAvailableWebhookEvents(c *echo.Context) error { return c.JSON(http.StatusOK, models.GetAvailableWebhookEvents()) } diff --git a/pkg/routes/api_tokens.go b/pkg/routes/api_tokens.go index 73de7c1fc..860be063b 100644 --- a/pkg/routes/api_tokens.go +++ b/pkg/routes/api_tokens.go @@ -27,14 +27,14 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - echojwt "github.com/labstack/echo-jwt/v4" - "github.com/labstack/echo/v4" + echojwt "github.com/labstack/echo-jwt/v5" + "github.com/labstack/echo/v5" ) func SetupTokenMiddleware() echo.MiddlewareFunc { return echojwt.WithConfig(echojwt.Config{ SigningKey: []byte(config.ServiceJWTSecret.GetString()), - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { authHeader := c.Request().Header.Values("Authorization") if len(authHeader) == 0 { return false // let the jwt middleware handle invalid headers @@ -42,13 +42,6 @@ func SetupTokenMiddleware() echo.MiddlewareFunc { for _, s := range authHeader { if strings.HasPrefix(s, "Bearer "+models.APITokenPrefix) { - // If the route does not exist, skip the current handling and let the rest of echo's logic handle it - findCtx := c.Echo().NewContext(c.Request(), c.Response()) - c.Echo().Router().Find(c.Request().Method, echo.GetPath(c.Request()), findCtx) - if findCtx.Path() == "/api/v1/*" { - return true - } - if c.Request().URL.Path == "/api/v1/token/test" { return true } @@ -60,9 +53,9 @@ func SetupTokenMiddleware() echo.MiddlewareFunc { return false }, - ErrorHandler: func(_ echo.Context, err error) error { + ErrorHandler: func(_ *echo.Context, err error) error { if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "missing, malformed, expired or otherwise invalid token provided").SetInternal(err) + return echo.NewHTTPError(http.StatusUnauthorized, "missing, malformed, expired or otherwise invalid token provided") } return nil @@ -70,27 +63,27 @@ func SetupTokenMiddleware() echo.MiddlewareFunc { }) } -func checkAPITokenAndPutItInContext(tokenHeaderValue string, c echo.Context) error { +func checkAPITokenAndPutItInContext(tokenHeaderValue string, c *echo.Context) error { s := db.NewSession() defer s.Close() token, err := models.GetTokenFromTokenString(s, strings.TrimPrefix(tokenHeaderValue, "Bearer ")) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error").Wrap(err) } if time.Now().After(token.ExpiresAt) { log.Debugf("[auth] Tried authenticating with token %d but it expired on %s", token.ID, token.ExpiresAt.String()) - return echo.NewHTTPError(http.StatusUnauthorized) + return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized") } if !models.CanDoAPIRoute(c, token) { log.Debugf("[auth] Tried authenticating with token %d but it does not have permission to do this route", token.ID) - return echo.NewHTTPError(http.StatusUnauthorized) + return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized") } u, err := user.GetUserByID(s, token.OwnerID) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error").Wrap(err) } c.Set("api_token", token) diff --git a/pkg/routes/caldav/auth.go b/pkg/routes/caldav/auth.go index d1e102443..3af6b6790 100644 --- a/pkg/routes/caldav/auth.go +++ b/pkg/routes/caldav/auth.go @@ -24,11 +24,11 @@ import ( "code.vikunja.io/api/pkg/user" "xorm.io/xorm" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "golang.org/x/crypto/bcrypt" ) -func BasicAuth(username, password string, c echo.Context) (bool, error) { +func BasicAuth(c *echo.Context, username, password string) (bool, error) { s := db.NewSession() defer s.Close() diff --git a/pkg/routes/caldav/handler.go b/pkg/routes/caldav/handler.go index 7279df6ab..c4d3e14fb 100644 --- a/pkg/routes/caldav/handler.go +++ b/pkg/routes/caldav/handler.go @@ -31,12 +31,12 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/samedi/caldav-go" "github.com/samedi/caldav-go/lib" ) -func getBasicAuthUserFromContext(c echo.Context) (*user.User, error) { +func getBasicAuthUserFromContext(c *echo.Context) (*user.User, error) { u, is := c.Get("userBasicAuth").(*user.User) if !is { return &user.User{}, fmt.Errorf("user is not user element, is %s", reflect.TypeOf(c.Get("userBasicAuth"))) @@ -45,7 +45,7 @@ func getBasicAuthUserFromContext(c echo.Context) (*user.User, error) { } // ProjectHandler returns all tasks from a project -func ProjectHandler(c echo.Context) error { +func ProjectHandler(c *echo.Context) error { project, err := getProjectFromParam(c) if err != nil && models.IsErrProjectDoesNotExist(err) { return c.String(http.StatusNotFound, "Project not found") @@ -56,7 +56,7 @@ func ProjectHandler(c echo.Context) error { u, err := getBasicAuthUserFromContext(c) if err != nil { - return echo.ErrInternalServerError.SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error").Wrap(err) } storage := &VikunjaCaldavProjectStorage{ @@ -90,7 +90,7 @@ func ProjectHandler(c echo.Context) error { } // TaskHandler is the handler which manages updating/deleting a single task -func TaskHandler(c echo.Context) error { +func TaskHandler(c *echo.Context) error { project, err := getProjectFromParam(c) if err != nil { return err @@ -98,7 +98,7 @@ func TaskHandler(c echo.Context) error { u, err := getBasicAuthUserFromContext(c) if err != nil { - return echo.ErrInternalServerError.SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error").Wrap(err) } // Get the task uid @@ -117,10 +117,10 @@ func TaskHandler(c echo.Context) error { } // PrincipalHandler handles all request to principal resources -func PrincipalHandler(c echo.Context) error { +func PrincipalHandler(c *echo.Context) error { u, err := getBasicAuthUserFromContext(c) if err != nil { - return echo.ErrInternalServerError.SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error").Wrap(err) } storage := &VikunjaCaldavProjectStorage{ @@ -146,10 +146,10 @@ func PrincipalHandler(c echo.Context) error { } // EntryHandler handles all request to principal resources -func EntryHandler(c echo.Context) error { +func EntryHandler(c *echo.Context) error { u, err := getBasicAuthUserFromContext(c) if err != nil { - return echo.ErrInternalServerError.SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error").Wrap(err) } storage := &VikunjaCaldavProjectStorage{ @@ -174,7 +174,7 @@ func EntryHandler(c echo.Context) error { return nil } -func getProjectFromParam(c echo.Context) (project *models.ProjectWithTasksAndBuckets, err error) { +func getProjectFromParam(c *echo.Context) (project *models.ProjectWithTasksAndBuckets, err error) { param := c.Param("project") if param == "" { return &models.ProjectWithTasksAndBuckets{}, nil diff --git a/pkg/routes/error_handler.go b/pkg/routes/error_handler.go index 0d4121772..d26ee0212 100644 --- a/pkg/routes/error_handler.go +++ b/pkg/routes/error_handler.go @@ -26,8 +26,7 @@ import ( "code.vikunja.io/api/pkg/web" "github.com/getsentry/sentry-go" - sentryecho "github.com/getsentry/sentry-go/echo" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // httpCodeGetter is an interface for errors that can provide their HTTP status code. @@ -46,8 +45,10 @@ type errorMessage struct { // 3. Handles Sentry reporting for 5xx errors // 4. Logs all errors appropriately func CreateHTTPErrorHandler(e *echo.Echo, enableSentry bool) echo.HTTPErrorHandler { - return func(err error, c echo.Context) { - if c.Response().Committed { + return func(c *echo.Context, err error) { + // Check if the response has already been committed (e.g., by the RequestLogger middleware + // with HandleError=true). If so, we should not try to write another response. + if r, _ := echo.UnwrapResponse(c.Response()); r != nil && r.Committed { return } @@ -59,43 +60,48 @@ func CreateHTTPErrorHandler(e *echo.Echo, enableSentry bool) echo.HTTPErrorHandl // Keep track of the original error for logging/sentry originalErr := err - // 1. Check if it's already an echo.HTTPError (from middleware, auth, etc.) + // 1. Check if it implements HTTPStatusCoder (includes echo.ErrForbidden, etc.) + // In Echo v5, predefined errors like ErrForbidden are *httpError (unexported), + // not *HTTPError, so we must check the interface instead of the concrete type. + var sc echo.HTTPStatusCoder + if errors.As(err, &sc) { + code = sc.StatusCode() + // HTTPStatusCoder doesn't have Error(), so we use the status text + message = http.StatusText(code) + } + + // 2. If it's specifically an HTTPError, use its message for more details var he *echo.HTTPError if errors.As(err, &he) { code = he.Code - message = he.Message - // Check if internal error has more details we should use - if he.Internal != nil { - originalErr = he.Internal - err = he.Internal + if he.Message != "" { + message = he.Message } } - // 2. Special case: 413 body limit → convert to ErrFileIsTooLarge - // This must be checked before other error type checks - if code == http.StatusRequestEntityTooLarge { + // 3. Special case: 413 body limit → convert to ErrFileIsTooLarge + // Check both the code (if it was an HTTPError) and errors.Is for wrapped errors + // In Echo v5, body limit errors during multipart parsing may be wrapped + if code == http.StatusRequestEntityTooLarge || errors.Is(err, echo.ErrStatusRequestEntityTooLarge) { fileErr := files.ErrFileIsTooLarge{} errDetails := fileErr.HTTPError() code = errDetails.HTTPCode message = errDetails } else if _, isMarshaler := err.(json.Marshaler); isMarshaler { - // 3. Check for json.Marshaler (preserves full struct like ValidationHTTPError) + // 4. Check for json.Marshaler (preserves full struct like ValidationHTTPError) // This allows errors with extra fields (like InvalidFields) to be serialized correctly if codeGetter, hasCode := err.(httpCodeGetter); hasCode { code = codeGetter.GetHTTPCode() } message = err // Echo will serialize via MarshalJSON } else if hp, ok := err.(web.HTTPErrorProcessor); ok { - // 4. Standard HTTPErrorProcessor (domain errors like ErrProjectDoesNotExist) + // 5. Standard HTTPErrorProcessor (domain errors like ErrProjectDoesNotExist) errDetails := hp.HTTPError() code = errDetails.HTTPCode message = errDetails } - // 5. For any other error type, we keep the defaults (500 with generic message) - // or the echo.HTTPError values if it was that type - - // Log the error - log.Error(originalErr.Error()) + // 6. For any other error type, we keep the defaults (500 with generic message) + // or the echo.HTTPStatusCoder/HTTPError values if it was that type // Sentry reporting for 5xx errors if enableSentry && code >= 500 { @@ -114,14 +120,14 @@ func CreateHTTPErrorHandler(e *echo.Echo, enableSentry bool) echo.HTTPErrorHandl err = c.JSON(code, message) } if err != nil { - e.Logger.Error(err) + e.Logger.Error(err.Error()) } } } // reportToSentry sends an error to Sentry with request context -func reportToSentry(err error, c echo.Context) { - hub := sentryecho.GetHubFromContext(c) +func reportToSentry(err error, c *echo.Context) { + hub := GetSentryHubFromContext(c) if hub != nil { hub.WithScope(func(scope *sentry.Scope) { scope.SetExtra("url", c.Request().URL) diff --git a/pkg/routes/healthcheck.go b/pkg/routes/healthcheck.go index fe76b01b9..a40046bf7 100644 --- a/pkg/routes/healthcheck.go +++ b/pkg/routes/healthcheck.go @@ -19,15 +19,15 @@ package routes import ( "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "code.vikunja.io/api/pkg/health" ) // HealthcheckHandler handles healthckeck 'OK' response -func HealthcheckHandler(c echo.Context) error { +func HealthcheckHandler(c *echo.Context) error { if err := health.Check(); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error").Wrap(err) } return c.String(http.StatusOK, "OK") } diff --git a/pkg/routes/metrics.go b/pkg/routes/metrics.go index 53621c6f3..3b5ba3575 100644 --- a/pkg/routes/metrics.go +++ b/pkg/routes/metrics.go @@ -27,8 +27,8 @@ import ( auth2 "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -83,7 +83,7 @@ func setupMetrics(a *echo.Group) { r := a.Group("/metrics") if config.MetricsUsername.GetString() != "" && config.MetricsPassword.GetString() != "" { - r.Use(middleware.BasicAuth(func(username, password string, _ echo.Context) (bool, error) { + r.Use(middleware.BasicAuth(func(_ *echo.Context, username, password string) (bool, error) { if subtle.ConstantTimeCompare([]byte(username), []byte(config.MetricsUsername.GetString())) == 1 && subtle.ConstantTimeCompare([]byte(password), []byte(config.MetricsPassword.GetString())) == 1 { return true, nil @@ -101,7 +101,7 @@ func setupMetricsMiddleware(a *echo.Group) { } a.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { // Update currently active users if err := updateActiveUsersFromContext(c); err != nil { @@ -114,7 +114,7 @@ func setupMetricsMiddleware(a *echo.Group) { } // updateActiveUsersFromContext updates the currently active users in redis -func updateActiveUsersFromContext(c echo.Context) (err error) { +func updateActiveUsersFromContext(c *echo.Context) (err error) { auth, err := auth2.GetAuthFromClaims(c) if err != nil { return diff --git a/pkg/routes/rate_limit.go b/pkg/routes/rate_limit.go index 85505d9e3..8ef22ef56 100644 --- a/pkg/routes/rate_limit.go +++ b/pkg/routes/rate_limit.go @@ -25,7 +25,7 @@ import ( "code.vikunja.io/api/pkg/log" auth2 "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/api/pkg/red" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3/drivers/store/memory" "github.com/ulule/limiter/v3/drivers/store/redis" @@ -34,7 +34,7 @@ import ( // RateLimit is the rate limit middleware func RateLimit(rateLimiter *limiter.Limiter, rateLimitKind string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) (err error) { + return func(c *echo.Context) (err error) { var rateLimitKey string switch rateLimitKind { case "ip": @@ -51,9 +51,7 @@ func RateLimit(rateLimiter *limiter.Limiter, rateLimitKind string) echo.Middlewa limiterCtx, err := rateLimiter.Get(c.Request().Context(), rateLimitKey) if err != nil { log.Errorf("IPRateLimit - rateLimiter.Get - err: %v, %s on %s", err, rateLimitKey, c.Request().URL) - return c.JSON(http.StatusInternalServerError, echo.Map{ - "message": err, - }) + return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error").Wrap(err) } h := c.Response().Header() @@ -63,9 +61,7 @@ func RateLimit(rateLimiter *limiter.Limiter, rateLimitKind string) echo.Middlewa if limiterCtx.Reached { log.Infof("Too Many Requests from %s on %s", rateLimitKey, c.Request().URL) - return c.JSON(http.StatusTooManyRequests, echo.Map{ - "message": "Too Many Requests on " + c.Request().URL.String(), - }) + return echo.NewHTTPError(http.StatusTooManyRequests, "Too Many Requests") } // log.Printf("%s request continue", c.RealIP()) diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 467d0a0f3..d078352e0 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -52,9 +52,8 @@ package routes import ( - "fmt" + "context" "log/slog" - "net/url" "strings" "time" @@ -80,51 +79,83 @@ import ( "code.vikunja.io/api/pkg/web/handler" "github.com/getsentry/sentry-go" - sentryecho "github.com/getsentry/sentry-go/echo" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "github.com/ulule/limiter/v3" ) -// slogHTTPMiddleware creates a custom HTTP logging middleware using slog -func slogHTTPMiddleware(logger *slog.Logger) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return echo.HandlerFunc(func(c echo.Context) error { - start := time.Now() - - err := next(c) - if err != nil { - c.Error(err) +// matchCORSOrigin checks if an origin matches any of the allowed origin patterns. +// It supports wildcards in the port position (e.g., "http://127.0.0.1:*"). +func matchCORSOrigin(origin string, allowedOrigins []string) (string, bool, error) { + for _, pattern := range allowedOrigins { + // Exact match + if origin == pattern { + return origin, true, nil + } + // Allow all + if pattern == "*" { + return origin, true, nil + } + // Handle wildcard port patterns like "http://127.0.0.1:*" or "http://localhost:*" + if strings.HasSuffix(pattern, ":*") { + prefix := strings.TrimSuffix(pattern, ":*") + // Check if the origin starts with the prefix and has a port after + if strings.HasPrefix(origin, prefix+":") { + return origin, true, nil } - - req := c.Request() - res := c.Response() - - logger.InfoContext(c.Request().Context(), - req.Method+" "+req.RequestURI, - "status", res.Status, - "remote_ip", c.RealIP(), - "latency", time.Since(start), - "user_agent", req.UserAgent(), - ) - - return err - }) + // Also match if origin has no port but pattern allows any port + if origin == prefix { + return origin, true, nil + } + } } + return "", false, nil } // NewEcho registers a new Echo instance func NewEcho() *echo.Echo { - e := echo.New() - - e.HideBanner = true + // Configure Echo with a router that unescapes path parameters. + // This is needed because Echo v5 does not unescape path params by default. + // Without this, path parameters like usernames with spaces or apostrophes + // would remain URL-encoded (e.g., "John%20D%27Urso" instead of "John D'Urso"). + // See https://kolaente.dev/vikunja/vikunja/issues/1224 + e := echo.NewWithConfig(echo.Config{ + Router: echo.NewRouter(echo.RouterConfig{ + UnescapePathParamValues: true, + }), + }) e.Logger = log.NewEchoLogger(config.LogEnabled.GetBool(), config.LogHTTP.GetString(), config.LogFormat.GetString()) // Logger if config.LogEnabled.GetBool() && config.LogHTTP.GetString() != "off" { httpLogger := log.NewHTTPLogger(config.LogEnabled.GetBool(), config.LogHTTP.GetString(), config.LogFormat.GetString()) - e.Use(slogHTTPMiddleware(httpLogger)) + e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + LogMethod: true, + LogLatency: true, + HandleError: true, + LogValuesFunc: func(_ *echo.Context, v middleware.RequestLoggerValues) error { + if v.Error == nil { + httpLogger.LogAttrs(context.Background(), slog.LevelInfo, "", + slog.String("method", v.Method), + slog.String("uri", v.URI), + slog.Int("status", v.Status), + slog.Duration("latency", v.Latency), + ) + } else { + httpLogger.LogAttrs(context.Background(), slog.LevelError, "", + slog.String("method", v.Method), + slog.String("uri", v.URI), + slog.Int("status", v.Status), + slog.Duration("latency", v.Latency), + slog.String("err", v.Error.Error()), + ) + } + return nil + }, + })) } // panic recover @@ -137,7 +168,9 @@ func NewEcho() *echo.Echo { // Set body limit to allow file uploads up to the configured size // Add some overhead for multipart form data (headers, boundaries, etc.) - e.Use(middleware.BodyLimit(fmt.Sprintf("%dM", config.GetMaxFileSizeInMBytes()+2))) + maxFileSize := config.GetMaxFileSizeInMBytes() + // #nosec G115 - maxFileSize is a configuration value that won't exceed int64 max in practice + e.Use(middleware.BodyLimit((int64(maxFileSize) + 2) * 1024 * 1024)) // Set up centralized error handler e.HTTPErrorHandler = CreateHTTPErrorHandler(e, config.SentryEnabled.GetBool()) @@ -159,7 +192,7 @@ func setupSentry(e *echo.Echo) { } defer sentry.Flush(5 * time.Second) - e.Use(sentryecho.New(sentryecho.Options{ + e.Use(SentryMiddleware(SentryOptions{ Repanic: true, })) } @@ -184,11 +217,18 @@ func RegisterRoutes(e *echo.Echo) { // CORS if config.CorsEnable.GetBool() { - log.Debugf("CORS enabled with origins: %s", strings.Join(config.CorsOrigins.GetStringSlice(), ", ")) + allowedOrigins := config.CorsOrigins.GetStringSlice() + log.Infof("CORS enabled with origins: %s", strings.Join(allowedOrigins, ", ")) + + // Echo v5 CORS middleware is stricter and doesn't accept wildcards in ports like "http://127.0.0.1:*" + // We use UnsafeAllowOriginFunc to handle these patterns for backwards compatibility e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: config.CorsOrigins.GetStringSlice(), - MaxAge: config.CorsMaxAge.GetInt(), - Skipper: func(context echo.Context) bool { + AllowOrigins: []string{}, // Empty because we use UnsafeAllowOriginFunc + UnsafeAllowOriginFunc: func(_ *echo.Context, origin string) (string, bool, error) { + return matchCORSOrigin(origin, allowedOrigins) + }, + MaxAge: config.CorsMaxAge.GetInt(), + Skipper: func(context *echo.Context) bool { // Since it is not possible to register this middleware just for the api group, // we just disable it when for caldav requests. // Caldav requires OPTIONS requests to be answered in a specific manner, @@ -200,10 +240,45 @@ func RegisterRoutes(e *echo.Echo) { // API Routes a := e.Group("/api/v1") - e.OnAddRouteHandler = func(_ string, route echo.Route, _ echo.HandlerFunc, middlewares []echo.MiddlewareFunc) { - models.CollectRoutesForAPITokenUsage(route, middlewares) - } registerAPIRoutes(a) + + // Collect routes for API token permissions + // In Echo v5, we collect routes after registration using e.Router().Routes() + collectRoutesForAPITokens(e) +} + +// unauthenticatedAPIPaths contains paths that don't require JWT authentication +var unauthenticatedAPIPaths = map[string]bool{ + "/api/v1/register": true, + "/api/v1/user/password/token": true, + "/api/v1/user/password/reset": true, + "/api/v1/user/confirm": true, + "/api/v1/login": true, + "/api/v1/auth/openid/:provider/callback": true, + "/api/v1/test/:table": true, + "/api/v1/info": true, + "/api/v1/shares/:share/auth": true, + "/api/v1/docs.json": true, + "/api/v1/docs": true, + "/api/v1/metrics": true, +} + +// collectRoutesForAPITokens collects all routes for API token permission checking. +// In Echo v5, OnAddRouteHandler was removed, so we collect routes after registration. +func collectRoutesForAPITokens(e *echo.Echo) { + routeList := e.Router().Routes() + log.Debugf("Collecting %d routes for API token usage", len(routeList)) + for _, route := range routeList { + // Only process API routes + if !strings.HasPrefix(route.Path, "/api/v1") { + continue + } + + // Check if this route requires JWT authentication + requiresJWT := !unauthenticatedAPIPaths[route.Path] + + models.CollectRoutesForAPITokenUsage(route, requiresJWT) + } } func registerAPIRoutes(a *echo.Group) { @@ -213,25 +288,6 @@ func registerAPIRoutes(a *echo.Group) { n := a.Group("") setupRateLimit(n, "ip") - // Echo does not unescape url path params by default. To make sure values bound as :param in urls are passed - // properly to handlers, we use this middleware to unescape them. - // See https://kolaente.dev/vikunja/vikunja/issues/1224 - // See https://github.com/labstack/echo/issues/766 - a.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - params := make([]string, 0, len(c.ParamValues())) - for _, param := range c.ParamValues() { - p, err := url.PathUnescape(param) - if err != nil { - return err - } - params = append(params, p) - } - c.SetParamValues(params...) - return next(c) - } - }) - // Docs n.GET("/docs.json", apiv1.DocsJSON) n.GET("/docs", apiv1.RedocUI) diff --git a/pkg/routes/sentry_middleware.go b/pkg/routes/sentry_middleware.go new file mode 100644 index 000000000..144ccf593 --- /dev/null +++ b/pkg/routes/sentry_middleware.go @@ -0,0 +1,85 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package routes + +import ( + "context" + "net/http" + + "github.com/getsentry/sentry-go" + "github.com/labstack/echo/v5" +) + +// sentryHubKey is the context key for storing the Sentry hub +type sentryHubKey struct{} + +// SentryOptions holds options for the sentry middleware +type SentryOptions struct { + // Repanic configures whether to repanic after recovery + Repanic bool +} + +// SentryMiddleware returns a middleware that captures panics and reports them to Sentry. +// It also attaches a Sentry hub to the request context. +func SentryMiddleware(options SentryOptions) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + hub := sentry.GetHubFromContext(c.Request().Context()) + if hub == nil { + hub = sentry.CurrentHub().Clone() + } + + scope := hub.Scope() + scope.SetRequest(c.Request()) + scope.SetRequestBody(nil) // We don't want to log request bodies + + // Store hub in context + ctx := context.WithValue(c.Request().Context(), sentryHubKey{}, hub) + c.SetRequest(c.Request().WithContext(ctx)) + + defer func() { + if err := recover(); err != nil { + eventID := hub.RecoverWithContext( + context.WithValue(c.Request().Context(), sentry.RequestContextKey, c.Request()), + err, + ) + if eventID != nil && options.Repanic { + panic(err) + } + } + }() + + return next(c) + } + } +} + +// GetSentryHubFromContext retrieves the Sentry hub from the echo context +func GetSentryHubFromContext(c *echo.Context) *sentry.Hub { + if hub, ok := c.Request().Context().Value(sentryHubKey{}).(*sentry.Hub); ok { + return hub + } + return nil +} + +// GetSentryHubFromRequest retrieves the Sentry hub from the HTTP request context +func GetSentryHubFromRequest(r *http.Request) *sentry.Hub { + if hub, ok := r.Context().Value(sentryHubKey{}).(*sentry.Hub); ok { + return hub + } + return nil +} diff --git a/pkg/routes/static.go b/pkg/routes/static.go index cd55cd2eb..74083c2f7 100644 --- a/pkg/routes/static.go +++ b/pkg/routes/static.go @@ -35,8 +35,8 @@ import ( "code.vikunja.io/api/pkg/config" etaggenerator "github.com/hhsnopek/etag" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) const ( @@ -67,7 +67,7 @@ func init() { scriptConfigStringLock = sync.Mutex{} } -func serveIndexFile(c echo.Context, assetFs http.FileSystem) (err error) { +func serveIndexFile(c *echo.Context, assetFs http.FileSystem) (err error) { index, err := assetFs.Open(path.Join(rootPath, indexFile)) if err != nil { return err @@ -140,7 +140,7 @@ func static() echo.MiddlewareFunc { assetFs := http.FS(frontend.Files) return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) (err error) { + return func(c *echo.Context) (err error) { p := c.Request().URL.Path if strings.HasPrefix(p, "/api/") { return next(c) @@ -268,7 +268,7 @@ func getCacheControlHeader(info os.FileInfo, file io.ReadSeeker) (header string, return cacheControlNone, nil } -func serveFile(c echo.Context, file io.ReadSeeker, info os.FileInfo, etag string) error { +func serveFile(c *echo.Context, file io.ReadSeeker, info os.FileInfo, etag string) error { c.Response().Header().Set("Server", "Vikunja") c.Response().Header().Set("Vary", "Accept-Encoding") @@ -290,7 +290,7 @@ func setupStaticFrontendFilesHandler(e *echo.Echo) { e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ Level: 6, MinLength: 256, - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { return strings.HasPrefix(c.Path(), "/api/") }, })) diff --git a/pkg/user/user.go b/pkg/user/user.go index 1bcefa860..bf79b5aba 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -33,7 +33,7 @@ import ( "code.vikunja.io/api/pkg/web" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "golang.org/x/crypto/bcrypt" "xorm.io/builder" "xorm.io/xorm" @@ -425,7 +425,7 @@ func CheckUserPassword(user *User, password string) error { } // GetCurrentUserFromDB gets a user from jwt claims and returns the full user from the db. -func GetCurrentUserFromDB(s *xorm.Session, c echo.Context) (user *User, err error) { +func GetCurrentUserFromDB(s *xorm.Session, c *echo.Context) (user *User, err error) { u, err := GetCurrentUser(c) if err != nil { return nil, err @@ -435,7 +435,7 @@ func GetCurrentUserFromDB(s *xorm.Session, c echo.Context) (user *User, err erro } // GetCurrentUser returns the current user based on its jwt token -func GetCurrentUser(c echo.Context) (user *User, err error) { +func GetCurrentUser(c *echo.Context) (user *User, err error) { if apiUser, ok := c.Get("api_user").(*User); ok { return apiUser, nil } diff --git a/pkg/web/handler/create.go b/pkg/web/handler/create.go index 3595f15c1..5cb5feaa1 100644 --- a/pkg/web/handler/create.go +++ b/pkg/web/handler/create.go @@ -26,11 +26,11 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // CreateWeb is the handler to create an object -func (c *WebHandler) CreateWeb(ctx echo.Context) error { +func (c *WebHandler) CreateWeb(ctx *echo.Context) error { // Get our model currentStruct := c.EmptyStruct() @@ -52,7 +52,7 @@ func (c *WebHandler) CreateWeb(ctx echo.Context) error { // Get the user to pass for later checks currentAuth, err := auth.GetAuthFromClaims(ctx) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.").SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.").Wrap(err) } // Create the db session @@ -73,7 +73,7 @@ func (c *WebHandler) CreateWeb(ctx echo.Context) error { if !canCreate { _ = s.Rollback() log.Warningf("Tried to create while not having the permissions for it (User: %v)", currentAuth) - return echo.NewHTTPError(http.StatusForbidden) + return echo.NewHTTPError(http.StatusForbidden, "Forbidden") } // Create diff --git a/pkg/web/handler/delete.go b/pkg/web/handler/delete.go index bb8fef4d8..55ba20f59 100644 --- a/pkg/web/handler/delete.go +++ b/pkg/web/handler/delete.go @@ -26,7 +26,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type message struct { @@ -34,7 +34,7 @@ type message struct { } // DeleteWeb is the web handler to delete something -func (c *WebHandler) DeleteWeb(ctx echo.Context) error { +func (c *WebHandler) DeleteWeb(ctx *echo.Context) error { // Get our model currentStruct := c.EmptyStruct() @@ -52,7 +52,7 @@ func (c *WebHandler) DeleteWeb(ctx echo.Context) error { // Check if the user has the permission to delete currentAuth, err := auth.GetAuthFromClaims(ctx) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.").Wrap(err) } // Create the db session @@ -72,7 +72,7 @@ func (c *WebHandler) DeleteWeb(ctx echo.Context) error { if !canDelete { _ = s.Rollback() log.Warningf("Tried to delete while not having the permissions for it (User: %v)", currentAuth) - return echo.NewHTTPError(http.StatusForbidden) + return echo.NewHTTPError(http.StatusForbidden, "Forbidden") } err = currentStruct.Delete(s, currentAuth) diff --git a/pkg/web/handler/read_all.go b/pkg/web/handler/read_all.go index 9afcc0290..9115a665f 100644 --- a/pkg/web/handler/read_all.go +++ b/pkg/web/handler/read_all.go @@ -30,17 +30,17 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // ReadAllWeb is the webhandler to get all objects of a type -func (c *WebHandler) ReadAllWeb(ctx echo.Context) error { +func (c *WebHandler) ReadAllWeb(ctx *echo.Context) error { // Get our model currentStruct := c.EmptyStruct() currentAuth, err := auth.GetAuthFromClaims(ctx) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.").SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.").Wrap(err) } // Get the object & bind params to struct @@ -61,7 +61,7 @@ func (c *WebHandler) ReadAllWeb(ctx echo.Context) error { pageNumber, err := strconv.Atoi(page) if err != nil { log.Error(err.Error()) - return echo.NewHTTPError(http.StatusBadRequest, "Bad page requested.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "Bad page requested.").Wrap(err) } if pageNumber < 0 { return echo.NewHTTPError(http.StatusBadRequest, "Page number cannot be negative.") @@ -76,7 +76,7 @@ func (c *WebHandler) ReadAllWeb(ctx echo.Context) error { perPageNumber, err = strconv.Atoi(perPage) if err != nil { log.Error(err.Error()) - return echo.NewHTTPError(http.StatusBadRequest, "Bad per page amount requested.").SetInternal(err) + return echo.NewHTTPError(http.StatusBadRequest, "Bad per page amount requested.").Wrap(err) } } // Set default page count diff --git a/pkg/web/handler/read_one.go b/pkg/web/handler/read_one.go index ceee70735..9d1c570c2 100644 --- a/pkg/web/handler/read_one.go +++ b/pkg/web/handler/read_one.go @@ -27,11 +27,11 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // ReadOneWeb is the webhandler to get one object -func (c *WebHandler) ReadOneWeb(ctx echo.Context) error { +func (c *WebHandler) ReadOneWeb(ctx *echo.Context) error { // Get our model currentStruct := c.EmptyStruct() @@ -48,7 +48,7 @@ func (c *WebHandler) ReadOneWeb(ctx echo.Context) error { // Check permissions currentAuth, err := auth.GetAuthFromClaims(ctx) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.").SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.").Wrap(err) } // Create the db session diff --git a/pkg/web/handler/update.go b/pkg/web/handler/update.go index 68b801ad8..2a0a80448 100644 --- a/pkg/web/handler/update.go +++ b/pkg/web/handler/update.go @@ -26,11 +26,11 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // UpdateWeb is the webhandler to update an object -func (c *WebHandler) UpdateWeb(ctx echo.Context) error { +func (c *WebHandler) UpdateWeb(ctx *echo.Context) error { // Get our model currentStruct := c.EmptyStruct() @@ -53,7 +53,7 @@ func (c *WebHandler) UpdateWeb(ctx echo.Context) error { // Check if the user has the permission to do that currentAuth, err := auth.GetAuthFromClaims(ctx) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.").SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.").Wrap(err) } // Create the db session @@ -73,7 +73,7 @@ func (c *WebHandler) UpdateWeb(ctx echo.Context) error { if !canUpdate { _ = s.Rollback() log.Warningf("Tried to update while not having the permissions for it (User: %v)", currentAuth) - return echo.NewHTTPError(http.StatusForbidden) + return echo.NewHTTPError(http.StatusForbidden, "Forbidden") } // Do the update diff --git a/pkg/web/web.go b/pkg/web/web.go index 4e724578a..98ef4f06c 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -17,7 +17,7 @@ package web import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "xorm.io/xorm" ) diff --git a/pkg/webtests/api_tokens_test.go b/pkg/webtests/api_tokens_test.go index 034ba76da..92acc9953 100644 --- a/pkg/webtests/api_tokens_test.go +++ b/pkg/webtests/api_tokens_test.go @@ -26,7 +26,7 @@ import ( "code.vikunja.io/api/pkg/routes" "code.vikunja.io/api/pkg/user" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,7 +38,7 @@ func TestAPIToken(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil) res := httptest.NewRecorder() c := e.NewContext(req, res) - h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + h := routes.SetupTokenMiddleware()(func(c *echo.Context) error { u, err := auth.GetAuthFromClaims(c) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) @@ -58,7 +58,7 @@ func TestAPIToken(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil) res := httptest.NewRecorder() c := e.NewContext(req, res) - h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + h := routes.SetupTokenMiddleware()(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) @@ -71,7 +71,7 @@ func TestAPIToken(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil) res := httptest.NewRecorder() c := e.NewContext(req, res) - h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + h := routes.SetupTokenMiddleware()(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) @@ -84,7 +84,7 @@ func TestAPIToken(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/projects", nil) res := httptest.NewRecorder() c := e.NewContext(req, res) - h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + h := routes.SetupTokenMiddleware()(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) @@ -97,7 +97,7 @@ func TestAPIToken(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil) res := httptest.NewRecorder() c := e.NewContext(req, res) - h := routes.SetupTokenMiddleware()(func(c echo.Context) error { + h := routes.SetupTokenMiddleware()(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) @@ -111,20 +111,4 @@ func TestAPIToken(t *testing.T) { req.Header.Set(echo.HeaderAuthorization, "Bearer "+jwt) require.NoError(t, h(c)) }) - t.Run("nonexisting route", func(t *testing.T) { - e, err := setupTestEnv() - require.NoError(t, err) - req := httptest.NewRequest(http.MethodGet, "/api/v1/nonexisting", nil) - res := httptest.NewRecorder() - c := e.NewContext(req, res) - h := routes.SetupTokenMiddleware()(func(c echo.Context) error { - return c.String(http.StatusNotFound, "test") - }) - - req.Header.Set(echo.HeaderAuthorization, "Bearer tk_a5e6f92ddbad68f49ee2c63e52174db0235008c8") // Token 2 - - err = h(c) - require.NoError(t, err) - assert.Equal(t, 404, c.Response().Status) - }) } diff --git a/pkg/webtests/integrations.go b/pkg/webtests/integrations.go index e7455643c..86cdd4de1 100644 --- a/pkg/webtests/integrations.go +++ b/pkg/webtests/integrations.go @@ -40,7 +40,7 @@ import ( "code.vikunja.io/api/pkg/web/handler" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -88,25 +88,26 @@ func setupTestEnv() (e *echo.Echo, err error) { return } -func createRequest(e *echo.Echo, method string, payload string, queryParam url.Values, urlParams map[string]string) (c echo.Context, rec *httptest.ResponseRecorder) { +func createRequest(e *echo.Echo, method string, payload string, queryParam url.Values, urlParams map[string]string) (c *echo.Context, rec *httptest.ResponseRecorder) { req := httptest.NewRequest(method, "/", strings.NewReader(payload)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) req.URL.RawQuery = queryParam.Encode() rec = httptest.NewRecorder() c = e.NewContext(req, rec) - var paramNames []string - var paramValues []string - for name, value := range urlParams { - paramNames = append(paramNames, name) - paramValues = append(paramValues, value) + // In Echo v5, we use SetPathValues to set path parameters + // Only set path values if there are any, as SetPathValues panics with nil + if len(urlParams) > 0 { + pathValues := make(echo.PathValues, 0, len(urlParams)) + for name, value := range urlParams { + pathValues = append(pathValues, echo.PathValue{Name: name, Value: value}) + } + c.SetPathValues(pathValues) } - c.SetParamNames(paramNames...) - c.SetParamValues(paramValues...) return } -func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values, urlParams map[string]string) (c echo.Context, rec *httptest.ResponseRecorder) { +func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values, urlParams map[string]string) (c *echo.Context, rec *httptest.ResponseRecorder) { // Setup e, err := setupTestEnv() require.NoError(t, err) @@ -115,14 +116,14 @@ func bootstrapTestRequest(t *testing.T, method string, payload string, queryPara return } -func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context) error, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { - var c echo.Context +func newTestRequest(t *testing.T, method string, handler func(ctx *echo.Context) error, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { + var c *echo.Context c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams) err = handler(c) return } -func addUserTokenToContext(t *testing.T, user *user.User, c echo.Context) { +func addUserTokenToContext(t *testing.T, user *user.User, c *echo.Context) { // Get the token as a string token, err := auth.NewUserJWTAuthtoken(user, false) require.NoError(t, err) @@ -134,7 +135,7 @@ func addUserTokenToContext(t *testing.T, user *user.User, c echo.Context) { c.Set("user", tken) } -func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c echo.Context) { +func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c *echo.Context) { // Get the token as a string token, err := auth.NewLinkShareJWTAuthtoken(share) require.NoError(t, err) @@ -147,7 +148,7 @@ func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c echo. } func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { - var c echo.Context + var c *echo.Context c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams) addUserTokenToContext(t, user, c) err = handler(c) @@ -155,7 +156,7 @@ func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFun } func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.HandlerFunc, share *models.LinkSharing, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { - var c echo.Context + var c *echo.Context c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams) addLinkShareTokenToContext(t, share, c) err = handler(c) @@ -163,11 +164,11 @@ func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.Handl } func newCaldavTestRequestWithUser(t *testing.T, e *echo.Echo, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { - var c echo.Context + var c *echo.Context c, rec = createRequest(e, method, payload, queryParams, urlParams) c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextPlain) - result, _ := caldav.BasicAuth(user.Username, "12345678", c) + result, _ := caldav.BasicAuth(c, user.Username, "12345678") if !result { t.Error("BasicAuth for caldav failed") t.FailNow() @@ -188,18 +189,34 @@ func assertHandlerErrorCode(t *testing.T, err error, expectedErrorCode int) { return } + // Try to unwrap to find HTTPErrorProcessor + unwrapped := errors.Unwrap(err) + for unwrapped != nil { + if httpErr, ok := unwrapped.(web.HTTPErrorProcessor); ok { + assert.Equal(t, expectedErrorCode, httpErr.HTTPError().Code) + return + } + unwrapped = errors.Unwrap(unwrapped) + } + // Fall back to echo.HTTPError for middleware/auth errors var httperr *echo.HTTPError if !errors.As(err, &httperr) { t.Errorf("Error is not *echo.HTTPError or web.HTTPErrorProcessor: %T", err) t.FailNow() } - webhttperr, ok := httperr.Message.(web.HTTPError) - if !ok { - t.Errorf("Error message is not web.HTTPError: %T", httperr.Message) - t.FailNow() + + // In Echo v5, HTTPError.Message is a string, not interface{} + // The internal error might contain our web.HTTPError + if innerErr := httperr.Unwrap(); innerErr != nil { + if httpErr, ok := innerErr.(web.HTTPErrorProcessor); ok { + assert.Equal(t, expectedErrorCode, httpErr.HTTPError().Code) + return + } } - assert.Equal(t, expectedErrorCode, webhttperr.Code) + + t.Errorf("Could not extract error code from error: %T - %v", err, err) + t.FailNow() } // getHTTPErrorCode extracts the HTTP status code from various error types diff --git a/pkg/webtests/task_attachment_upload_test.go b/pkg/webtests/task_attachment_upload_test.go index 12d2edabd..179da43d0 100644 --- a/pkg/webtests/task_attachment_upload_test.go +++ b/pkg/webtests/task_attachment_upload_test.go @@ -28,7 +28,7 @@ import ( "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/api/pkg/routes" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" )