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 <k@knt.li>
This commit is contained in:
renovate[bot]
2026-01-24 20:38:32 +01:00
committed by GitHub
parent 83474b76d3
commit 9a61453e86
63 changed files with 667 additions and 617 deletions

View File

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

8
go.mod
View File

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

26
go.sum
View File

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

View File

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

View File

@@ -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")
}

View File

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

View File

@@ -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"),

View File

@@ -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"),

View File

@@ -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"),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@
package plugins
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
"src.techknowlogick.com/xormigrate"
)

View File

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

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 == "" {

View File

@@ -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())
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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")
}

View File

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

View File

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

View File

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

View File

@@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@@ -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/")
},
}))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@
package web
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
"xorm.io/xorm"
)

View File

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

View File

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

View File

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