mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-02-01 22:47:40 +00:00
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:
@@ -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
8
go.mod
@@ -37,7 +37,6 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.12
|
||||
github.com/ganigeorgiev/fexpr v0.5.0
|
||||
github.com/getsentry/sentry-go v0.41.0
|
||||
github.com/getsentry/sentry-go/echo v0.41.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.19.0
|
||||
@@ -51,11 +50,8 @@ require (
|
||||
github.com/jaswdr/faker/v2 v2.9.1
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6
|
||||
github.com/labstack/echo-jwt/v4 v4.4.0
|
||||
github.com/labstack/echo-jwt/v5 v5.0.0
|
||||
github.com/labstack/echo/v4 v4.15.0
|
||||
github.com/labstack/echo/v5 v5.0.0
|
||||
github.com/labstack/gommon v0.4.2
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/magefile/mage v1.15.0
|
||||
github.com/mattn/go-sqlite3 v1.14.33
|
||||
@@ -172,8 +168,6 @@ require (
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/tj/assert v0.0.3 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
@@ -189,8 +183,6 @@ require (
|
||||
|
||||
replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
|
||||
|
||||
replace github.com/labstack/echo/v4 => github.com/kolaente/echo/v4 v4.0.0-20250124112709-682dfde74c31 // https://github.com/labstack/echo/pull/2738
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.25.6
|
||||
|
||||
26
go.sum
26
go.sum
@@ -94,8 +94,6 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
|
||||
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
@@ -132,8 +130,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b h1:+0Xqob+onh+4l9TSWmFyZ4JHqGUiCy5P1muyH8Evfpw=
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fclairamb/afero-s3 v0.4.0 h1:N++eKFyOTkdYQMDSAU2AGMVWBOo49FqbpeQ+e3v1jQA=
|
||||
@@ -148,14 +144,8 @@ github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCK
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
|
||||
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/getsentry/sentry-go v0.41.0 h1:q/dQZOlEIb4lhxQSjJhQqtRr3vwrJ6Ahe1C9zv+ryRo=
|
||||
github.com/getsentry/sentry-go v0.41.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/getsentry/sentry-go/echo v0.40.0 h1:6vAmqHZbloXwGmESjtTqroti+MI8odvXtEo6PSOP0r0=
|
||||
github.com/getsentry/sentry-go/echo v0.40.0/go.mod h1:UOd1hu1AlkrJrUm5vJtWfg4k/fnPRxkiDm3gxpNQ6cs=
|
||||
github.com/getsentry/sentry-go/echo v0.41.0 h1:f4dL3KlNI8iTD+30dUQEWG/NTJnWEkqalV2Lw90sX40=
|
||||
github.com/getsentry/sentry-go/echo v0.41.0/go.mod h1:qfdk4TD+SIrtwOlYWW+3TAX+taksREddhzaKCe1+2eQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
@@ -324,8 +314,6 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible h1:q7DbyV+sFjEoTuuUdRDNl2nlyfztkZgxVVCV7JhzIkY=
|
||||
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
|
||||
github.com/kolaente/echo/v4 v4.0.0-20250124112709-682dfde74c31 h1:lUUZppO9AB30mfNALYcMAbr32XJtZG4itqG21crNAlQ=
|
||||
github.com/kolaente/echo/v4 v4.0.0-20250124112709-682dfde74c31/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -338,12 +326,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo-jwt/v4 v4.4.0 h1:nrXaEnJupfc2R4XChcLRDyghhMZup77F8nIzHnBK19U=
|
||||
github.com/labstack/echo-jwt/v4 v4.4.0/go.mod h1:kYXWgWms9iFqI3ldR+HAEj/Zfg5rZtR7ePOgktG4Hjg=
|
||||
github.com/labstack/echo-jwt/v5 v5.0.0 h1:uPp+FpkI/PKpMPPygtnK3RQOpg5a2wlM04UgfpWLVyI=
|
||||
github.com/labstack/echo-jwt/v5 v5.0.0/go.mod h1:RYF2ojWXbaY09QQ5J9vVtPUtkyI5UztS0gJotmCRz/U=
|
||||
github.com/labstack/echo/v5 v5.0.0 h1:JHKGrI0cbNsNMyKvranuY0C94O4hSM7yc/HtwcV3Na4=
|
||||
github.com/labstack/echo/v5 v5.0.0/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef h1:RZnRnSID1skF35j/15KJ6hKZkdIC/teQClJK5wP5LU4=
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
@@ -402,12 +388,8 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
|
||||
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
||||
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
|
||||
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
||||
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
|
||||
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
|
||||
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
|
||||
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -515,10 +497,6 @@ github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2t
|
||||
github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
85
pkg/routes/sentry_middleware.go
Normal file
85
pkg/routes/sentry_middleware.go
Normal 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
|
||||
}
|
||||
@@ -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/")
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user