diff --git a/go.mod b/go.mod
index 814e325..93e7008 100644
--- a/go.mod
+++ b/go.mod
@@ -4,15 +4,13 @@ go 1.25.3
require (
github.com/a-h/templ v0.3.960
- github.com/getsentry/sentry-go v0.36.0
- github.com/getsentry/sentry-go/echo v0.36.0
+ github.com/getsentry/sentry-go v0.36.2
+ github.com/go-chi/chi/v5 v5.2.3
+ github.com/go-chi/cors v1.2.2
github.com/gorilla/sessions v1.4.0
github.com/joho/godotenv v1.5.1
- github.com/labstack/echo-contrib v0.17.4
- github.com/labstack/echo/v4 v4.13.4
- github.com/labstack/gommon v0.4.2
github.com/rjNemo/underscore v0.8.0
- github.com/stripe/stripe-go/v83 v83.0.1
+ github.com/stripe/stripe-go/v83 v83.1.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.0
)
@@ -26,7 +24,6 @@ require (
)
require (
- github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@@ -34,16 +31,11 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
- github.com/mattn/go-colorable v0.1.14 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
github.com/openai/openai-go v1.12.0
github.com/sethvargo/go-envconfig v1.3.0
- github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
- golang.org/x/time v0.14.0 // indirect
)
diff --git a/go.sum b/go.sum
index dbbac39..86d62cf 100644
--- a/go.sum
+++ b/go.sum
@@ -3,18 +3,18 @@ github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/getsentry/sentry-go v0.36.0 h1:UkCk0zV28PiGf+2YIONSSYiYhxwlERE5Li3JPpZqEns=
-github.com/getsentry/sentry-go v0.36.0/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c=
-github.com/getsentry/sentry-go/echo v0.36.0 h1:PimJIxiH2O/nS+jegFLxx52RMpVY2ciAIvVkk8miVeM=
-github.com/getsentry/sentry-go/echo v0.36.0/go.mod h1:Z4Q44b9OWBO18lFcC1yfCqOVex00nz2WPSH1AuUUC5I=
+github.com/getsentry/sentry-go v0.36.2 h1:uhuxRPTrUy0dnSzTd0LrYXlBYygLkKY0hhlG5LXarzM=
+github.com/getsentry/sentry-go v0.36.2/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c=
+github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
+github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
+github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
-github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
@@ -33,16 +33,6 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
-github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
-github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
-github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
-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/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0=
github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
@@ -60,8 +50,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/stripe/stripe-go/v83 v83.0.1 h1:HvUXOw0AcjYJ9zUTN5XW+k7HvkM1AY9zxbpOFN9bhRA=
-github.com/stripe/stripe-go/v83 v83.0.1/go.mod h1:nRyDcLrJtwPPQUnKAFs9Bt1NnQvNhNiF6V19XHmPISE=
+github.com/stripe/stripe-go/v83 v83.1.0 h1:h6Wi8+dSUCmIdXDWObs1AirP9tQGWWI/4xP5oE5G6uQ=
+github.com/stripe/stripe-go/v83 v83.1.0/go.mod h1:nRyDcLrJtwPPQUnKAFs9Bt1NnQvNhNiF6V19XHmPISE=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -73,10 +63,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
-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=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
@@ -85,13 +71,10 @@ golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
-golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
-golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/internal/cron/job_stripe_sync.go b/internal/cron/job_stripe_sync.go
index 0c75ddf..3ff25ad 100644
--- a/internal/cron/job_stripe_sync.go
+++ b/internal/cron/job_stripe_sync.go
@@ -41,9 +41,6 @@ func JobStripePaymentSync() error {
store := booking.NewPgStore(db)
opts := []stripeclient.Option{}
- if cfg.StripeConnectAccount != "" {
- opts = append(opts, stripeclient.WithAccount(cfg.StripeConnectAccount))
- }
client, err := stripeclient.New(cfg.StripeSecretKey, opts...)
if err != nil {
diff --git a/internal/server/auth.go b/internal/server/auth.go
index 69d1bb6..c1d8773 100644
--- a/internal/server/auth.go
+++ b/internal/server/auth.go
@@ -3,23 +3,24 @@ package server
import (
"net/http"
- "github.com/labstack/echo/v4"
-
"github.com/rjNemo/rentease/internal/constant"
"github.com/rjNemo/rentease/internal/service/auth"
)
-func MakeAuthMiddleware(as *auth.Service) echo.MiddlewareFunc {
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- if c.Request().RequestURI == constant.RouteLogin {
- return next(c)
+func MakeAuthMiddleware(as *auth.Service) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == constant.RouteLogin {
+ next.ServeHTTP(w, r)
+ return
}
- if !as.Authenticated(c) {
- return c.Redirect(http.StatusSeeOther, constant.RouteLogin)
+ if !as.Authenticated(r) {
+ http.Redirect(w, r, constant.RouteLogin, http.StatusSeeOther)
+ return
}
- return next(c)
- }
+
+ next.ServeHTTP(w, r)
+ })
}
}
diff --git a/internal/server/handle_api_sync.go b/internal/server/handle_api_sync.go
index 4ab979a..1cec945 100644
--- a/internal/server/handle_api_sync.go
+++ b/internal/server/handle_api_sync.go
@@ -4,68 +4,82 @@ import (
"encoding/json"
"fmt"
"io"
+ "log/slog"
"net/http"
- "github.com/labstack/echo/v4"
- "github.com/labstack/gommon/log"
-
"github.com/rjNemo/rentease/internal/service/booking"
)
-func handleSync(bs *booking.Service) echo.HandlerFunc {
+func handleSync(bs *booking.Service) http.HandlerFunc {
type BookingInfo struct {
Content string `json:"content"`
}
- return func(c echo.Context) error {
- log.Info("received booking sync request from booking")
- x := c.Request().Body
- body, err := io.ReadAll(x)
+ return func(w http.ResponseWriter, r *http.Request) {
+ slog.Info("received booking sync request from booking")
+ body, err := io.ReadAll(r.Body)
if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
+ defer r.Body.Close()
bookingInfo := new(BookingInfo)
- err = json.Unmarshal(body, bookingInfo)
- if err != nil {
- return c.String(http.StatusInternalServerError, fmt.Sprintf("error unmarshalling JSON: %s", err))
+ if err := json.Unmarshal(body, bookingInfo); err != nil {
+ http.Error(w, fmt.Sprintf("error unmarshalling JSON: %s", err), http.StatusInternalServerError)
+ return
}
b, err := bs.ParseFromAPI(bookingInfo.Content)
if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
- log.Infof("created booking %q from %q", b.Name, b.Platform)
- return c.JSON(http.StatusCreated, "👍")
+ slog.Info("created booking from API", slog.String("name", b.Name), slog.String("platform", string(b.Platform)))
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ if err := json.NewEncoder(w).Encode("👍"); err != nil {
+ slog.Error("failed to write response", slog.Any("error", err))
+ }
}
}
-func handleCreateBooking(bs *booking.Service) echo.HandlerFunc {
+func handleCreateBooking(bs *booking.Service) http.HandlerFunc {
type BookingInfo struct {
Content string `json:"content"`
}
- return func(c echo.Context) error {
- log.Info("received booking sync request from booking")
- x := c.Request().Body
- body, err := io.ReadAll(x)
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ slog.Info("received booking sync request from booking")
+ body, err := io.ReadAll(r.Body)
if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
- log.Info(string(body))
+ defer r.Body.Close()
+
+ slog.Info("request body", slog.String("body", string(body)))
bookingInfo := new(BookingInfo)
- err = json.Unmarshal(body, bookingInfo)
- if err != nil {
- return c.String(http.StatusInternalServerError, fmt.Sprintf("error unmarshalling JSON: %s", err))
+ if err := json.Unmarshal(body, bookingInfo); err != nil {
+ http.Error(w, fmt.Sprintf("error unmarshalling JSON: %s", err), http.StatusInternalServerError)
+ return
}
b, err := bs.ParseFromAPI(bookingInfo.Content)
if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
- log.Infof("created booking %q from %q", b.Name, b.Platform)
- return c.JSON(http.StatusCreated, "👍")
+ slog.Info("created booking from API", slog.String("name", b.Name), slog.String("platform", string(b.Platform)))
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ if err := json.NewEncoder(w).Encode("👍"); err != nil {
+ slog.Error("failed to write response", slog.Any("error", err))
+ }
}
}
diff --git a/internal/server/handle_auth.go b/internal/server/handle_auth.go
index b459af7..78a9316 100644
--- a/internal/server/handle_auth.go
+++ b/internal/server/handle_auth.go
@@ -3,23 +3,28 @@ package server
import (
"net/http"
- "github.com/labstack/echo/v4"
-
"github.com/rjNemo/rentease/internal/constant"
"github.com/rjNemo/rentease/internal/service/auth"
"github.com/rjNemo/rentease/internal/view"
)
-func handleLoginPage() echo.HandlerFunc {
- return func(c echo.Context) error {
- return renderTempl(c, http.StatusOK, view.Login(view.LoginFormViewModel{}))
+func handleLoginPage() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := renderTempl(w, http.StatusOK, view.Login(view.LoginFormViewModel{})); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handleLogin(as *auth.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- email := c.FormValue("email")
- password := c.FormValue("password")
+func handleLogin(as *auth.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ email := r.FormValue("email")
+ password := r.FormValue("password")
if !as.ValidCredentials(email, password) {
lfvm := view.LoginFormViewModel{
@@ -28,14 +33,19 @@ func handleLogin(as *auth.Service) echo.HandlerFunc {
Errors: make(map[string]string),
}
lfvm.Errors["credentials"] = "invalid credentials"
- return renderTempl(c, http.StatusUnauthorized, view.LoginForm(lfvm))
+ if err := renderTempl(w, http.StatusUnauthorized, view.LoginForm(lfvm)); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ return
}
- err := as.Authenticate(c, "foo")
- if err != nil {
- return err
+ if err := as.Authenticate(w, r, "foo"); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
- return hxRedirect(c, http.StatusOK, constant.RouteBooking)
+ if err := hxRedirect(w, http.StatusOK, constant.RouteBooking); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
diff --git a/internal/server/handle_bookings.go b/internal/server/handle_bookings.go
index 3c649a8..8b28fdf 100644
--- a/internal/server/handle_bookings.go
+++ b/internal/server/handle_bookings.go
@@ -1,7 +1,9 @@
package server
import (
+ "bytes"
"context"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -11,8 +13,7 @@ import (
"time"
"github.com/a-h/templ"
- "github.com/labstack/echo/v4"
- "github.com/labstack/gommon/log"
+ "github.com/go-chi/chi/v5"
u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/config"
@@ -22,9 +23,9 @@ import (
myTime "github.com/rjNemo/rentease/pkg/time"
)
-func handleBookingListPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
- return func(c echo.Context) error {
- search := c.FormValue("search")
+func handleBookingListPage(bs *booking.Service, hc *config.Host) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ search := r.URL.Query().Get("search")
var bookings []*booking.Line
if search != "" {
@@ -48,10 +49,16 @@ func handleBookingListPage(bs *booking.Service, hc *config.Host) echo.HandlerFun
}
})
- if hxRequest(c) && !hxBoosted(c) {
- return renderTempl(c, http.StatusOK, view.BookingLines(bvm))
- } else {
- return renderTempl(c, http.StatusOK, view.ListBookings(bvm))
+ var err error
+ switch {
+ case hxRequest(r) && !hxBoosted(r):
+ err = renderTempl(w, http.StatusOK, view.BookingLines(bvm))
+ default:
+ err = renderTempl(w, http.StatusOK, view.ListBookings(bvm))
+ }
+
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
@@ -70,9 +77,9 @@ func paymentViewModelFromBookingPayment(p booking.Payment) *view.PaymentViewMode
}
}
-func handleBookingList(bs *booking.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- search := c.FormValue("search")
+func handleBookingList(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ search := r.URL.Query().Get("search")
var bookings []*booking.Line
if search != "" {
@@ -81,57 +88,77 @@ func handleBookingList(bs *booking.Service) echo.HandlerFunc {
bookings = bs.All()
}
- return c.JSON(http.StatusOK, bookings)
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(bookings); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handleBookingCreatePage(hc *config.Host) echo.HandlerFunc {
- return func(c echo.Context) error {
- return renderTempl(c, http.StatusOK, view.NewBooking(u.Map(hc.Platforms, func(p config.Platform) string {
+func handleBookingCreatePage(hc *config.Host) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := renderTempl(w, http.StatusOK, view.NewBooking(u.Map(hc.Platforms, func(p config.Platform) string {
return string(p)
- })))
+ }))); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handleBookingCreate(bs *booking.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- type NewBooking struct {
- From time.Time `json:"from"`
- To time.Time `json:"to"`
- ExternalId *string `form:"external_id"`
- Name string `form:"name"`
- PhoneNumber string `form:"phone_number"`
- Email string `form:"email"`
- Platform string `form:"platform"`
- CustomerNumber int `form:"customer_number"`
- PlatformFees float64 `form:"platform_fees"`
+func handleBookingCreate(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
}
- nb := new(NewBooking)
- err := c.Bind(nb)
+
+ customerNumber, err := strconv.Atoi(r.FormValue("customer_number"))
if err != nil {
- log.Warn(err)
- return err
+ http.Error(w, "invalid customer number", http.StatusBadRequest)
+ return
}
- ts, _ := myTime.ParseFromForm(c.FormValue("from"))
- nb.From = ts
- ts, _ = myTime.ParseFromForm(c.FormValue("to"))
- nb.To = ts
-
- if *nb.ExternalId == "" {
- nb.ExternalId = nil
+ platformFees := 0.0
+ if v := r.FormValue("platform_fees"); v != "" {
+ platformFees, err = strconv.ParseFloat(v, 64)
+ if err != nil {
+ http.Error(w, "invalid platform fees", http.StatusBadRequest)
+ return
+ }
}
- b := bs.Create(nb.From, nb.To, nb.Name, nb.PhoneNumber, nb.Email, nb.Platform, nb.CustomerNumber, nb.PlatformFees, nb.ExternalId)
- return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%d", constant.RouteBooking, b.ID))
+
+ externalID := r.FormValue("external_id")
+ var externalPtr *string
+ if externalID != "" {
+ externalPtr = &externalID
+ }
+
+ from, _ := myTime.ParseFromForm(r.FormValue("from"))
+ to, _ := myTime.ParseFromForm(r.FormValue("to"))
+
+ b := bs.Create(
+ from,
+ to,
+ r.FormValue("name"),
+ r.FormValue("phone_number"),
+ r.FormValue("email"),
+ r.FormValue("platform"),
+ customerNumber,
+ platformFees,
+ externalPtr,
+ )
+
+ http.Redirect(w, r, fmt.Sprintf("%s/%d", constant.RouteBooking, b.ID), http.StatusSeeOther)
}
}
-func handleBookingPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
- return func(c echo.Context) error {
- idStr := c.Param("id")
+func handleBookingPage(bs *booking.Service, hc *config.Host) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
- return err
+ http.Error(w, "invalid booking id", http.StatusBadRequest)
+ return
}
b := bs.One(id)
@@ -189,67 +216,99 @@ func handleBookingPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
),
PaymentMethods: hc.PaymentMethods,
}
- return renderTempl(c, http.StatusOK, view.BookingById(bvm))
+
+ if err := renderTempl(w, http.StatusOK, view.BookingById(bvm)); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handleBookingStripePaymentLink(bs *booking.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- idStr := c.Param("id")
+func handleBookingStripePaymentLink(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "invalid booking id")
+ http.Error(w, "invalid booking id", http.StatusBadRequest)
+ return
}
- url, err := bs.CreateStripePaymentLink(c.Request().Context(), id)
+ url, err := bs.CreateStripePaymentLink(r.Context(), id)
if err != nil {
switch {
case errors.Is(err, booking.ErrStripeClientNotConfigured):
- return echo.NewHTTPError(http.StatusBadRequest, "stripe is not configured")
+ http.Error(w, "stripe is not configured", http.StatusBadRequest)
case errors.Is(err, booking.ErrBookingNotFound):
- return echo.NewHTTPError(http.StatusNotFound, "booking not found")
+ http.Error(w, "booking not found", http.StatusNotFound)
case errors.Is(err, booking.ErrNoOutstandingBalance):
- return echo.NewHTTPError(http.StatusBadRequest, "booking has no outstanding balance")
+ http.Error(w, "booking has no outstanding balance", http.StatusBadRequest)
default:
- return fmt.Errorf("failed to create payment link: %w", err)
+ http.Error(w, fmt.Sprintf("failed to create payment link: %v", err), http.StatusInternalServerError)
}
+ return
}
- return c.JSON(http.StatusCreated, map[string]string{"url": url})
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ if err := json.NewEncoder(w).Encode(map[string]string{"url": url}); err != nil {
+ slog.Error("failed to write stripe payment link response", slog.Any("error", err))
+ }
}
}
-func handleBookingUpdate(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
- return func(c echo.Context) error {
- type UpdateBooking struct {
- From time.Time `json:"from"`
- To time.Time `json:"to"`
- ExternalId *string `form:"external_id"`
- Name string `form:"name"`
- PhoneNumber string `form:"phone_number"`
- Email string `form:"email"`
- Platform string `form:"platform"`
- Id int `param:"id"`
- CustomerNumber int `form:"customer_number"`
- PlatformFees float64 `form:"platform_fees"`
+func handleBookingUpdate(bs *booking.Service, hc *config.Host) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
}
- nb := new(UpdateBooking)
- err := c.Bind(nb)
+
+ id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
- log.Warn(err)
- return err
+ http.Error(w, "invalid booking id", http.StatusBadRequest)
+ return
}
- ts, _ := myTime.ParseFromForm(c.FormValue("from"))
- nb.From = ts
- ts, _ = myTime.ParseFromForm(c.FormValue("to"))
- nb.To = ts
-
- if *nb.ExternalId == "" {
- nb.ExternalId = nil
+ customerNumber, err := strconv.Atoi(r.FormValue("customer_number"))
+ if err != nil {
+ http.Error(w, "invalid customer number", http.StatusBadRequest)
+ return
}
- b := bs.Update(nb.Id, nb.From, nb.To, nb.Name, nb.PhoneNumber, nb.Email, nb.Platform, nb.CustomerNumber, nb.PlatformFees, nb.ExternalId)
+ platformFees := 0.0
+ if v := r.FormValue("platform_fees"); v != "" {
+ platformFees, err = strconv.ParseFloat(v, 64)
+ if err != nil {
+ http.Error(w, "invalid platform fees", http.StatusBadRequest)
+ return
+ }
+ }
+
+ externalID := r.FormValue("external_id")
+ var externalPtr *string
+ if externalID != "" {
+ externalPtr = &externalID
+ }
+
+ from, _ := myTime.ParseFromForm(r.FormValue("from"))
+ to, _ := myTime.ParseFromForm(r.FormValue("to"))
+
+ b := bs.Update(
+ id,
+ from,
+ to,
+ r.FormValue("name"),
+ r.FormValue("phone_number"),
+ r.FormValue("email"),
+ r.FormValue("platform"),
+ customerNumber,
+ platformFees,
+ externalPtr,
+ )
+
+ externalValue := ""
+ if b.ExternalID != nil {
+ externalValue = *b.ExternalID
+ }
form := view.BookingForm(view.BookingViewModel{
Id: b.InvoiceNumber(hc),
@@ -261,7 +320,7 @@ func handleBookingUpdate(bs *booking.Service, hc *config.Host) echo.HandlerFunc
To: b.To.Format(time.DateOnly),
Canceled: b.Canceled,
Platform: string(b.Platform),
- ExternalId: *b.ExternalID,
+ ExternalId: externalValue,
Platforms: u.Map(hc.Platforms, func(p config.Platform) string { return string(p) }),
PlatformFees: strconv.FormatFloat(b.PlatformFees, 'f', 2, 64),
PaymentMethods: hc.PaymentMethods,
@@ -269,17 +328,21 @@ func handleBookingUpdate(bs *booking.Service, hc *config.Host) echo.HandlerFunc
PdfUrl: templ.SafeURL(fmt.Sprintf("%s/pdf/%d", constant.RouteBooking, b.ID)),
})
- return renderTempl(c, http.StatusOK, form)
+ if err := renderTempl(w, http.StatusOK, form); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handleLineItemForm(bs *booking.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- idStr := c.Param("id")
+func handleLineItemForm(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
- return err
+ http.Error(w, "invalid item id", http.StatusBadRequest)
+ return
}
+
i := bs.OneItem(id)
form := view.LineItemForm(&view.ItemViewModel{
Id: strconv.Itoa(i.ID),
@@ -290,53 +353,56 @@ func handleLineItemForm(bs *booking.Service) echo.HandlerFunc {
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.ID),
})
- return renderTempl(c, http.StatusOK, form)
+
+ if err := renderTempl(w, http.StatusOK, form); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handleCreateItem(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
- type NewItem struct {
- Item string `form:"item"`
- PaymentMethod string `form:"method"`
- Quantity int `form:"quantity"`
- Price float64 `form:"price"`
- }
-
- return func(c echo.Context) error {
- bookingIdStr := c.Param("id")
+func handleCreateItem(bs *booking.Service, hc *config.Host) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ bookingIdStr := chi.URLParam(r, "id")
bid, err := strconv.Atoi(bookingIdStr)
if err != nil {
- return err
+ http.Error(w, "invalid booking id", http.StatusBadRequest)
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
}
b := bs.One(bid)
- ni := new(NewItem)
- if err := c.Bind(ni); err != nil {
- log.Warn(err)
- return err
- }
-
- itm, ok := hc.Items[ni.Item]
+ itemName := r.FormValue("item")
+ itm, ok := hc.Items[itemName]
if !ok {
- return fmt.Errorf("invalid item name %q", ni.Item)
+ http.Error(w, fmt.Sprintf("invalid item name %q", itemName), http.StatusBadRequest)
+ return
}
- newItems := bs.CreateItem(b.ID, itm, ni.Quantity, ni.Price, ni.PaymentMethod, b.CustomerNumber, string(b.Platform))
+ quantity, err := strconv.Atoi(r.FormValue("quantity"))
+ if err != nil {
+ http.Error(w, "invalid quantity", http.StatusBadRequest)
+ return
+ }
- // TODO: fix the calendar integration
- // if err = cs.Create(
- // itm.CalendarId,
- // b.Name,
- // fmt.Sprintf("Reservation: %s\n %d voyageur(s)\n", b.Name, b.CustomerNumber),
- // b.From, b.To,
- // ); err != nil {
- // log.Warnf("could not create event: %s", err)
- // captureError(c, err)
- // }
+ price := 0.0
+ if v := r.FormValue("price"); v != "" {
+ price, err = strconv.ParseFloat(v, 64)
+ if err != nil {
+ http.Error(w, "invalid price", http.StatusBadRequest)
+ return
+ }
+ }
+ newItems := bs.CreateItem(b.ID, itm, quantity, price, r.FormValue("method"), b.CustomerNumber, string(b.Platform))
+
+ var buf bytes.Buffer
for _, i := range newItems {
- _ = renderTempl(c, http.StatusCreated, view.LineItem(&view.ItemViewModel{
+ component := view.LineItem(&view.ItemViewModel{
Id: strconv.Itoa(i.ID),
Item: i.Item,
Quantity: strconv.Itoa(i.Quantity),
@@ -344,23 +410,32 @@ func handleCreateItem(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
PaymentStatus: i.PaymentStatus,
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.ID),
- }))
+ })
+ if err := component.Render(context.Background(), &buf); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
}
- return nil
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusCreated)
+ if _, err := buf.WriteTo(w); err != nil {
+ slog.Error("failed to write item response", slog.Any("error", err))
+ }
}
}
-func handleItemPay(bs *booking.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- itemIdStr := c.Param("id")
+func handleItemPay(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ itemIdStr := chi.URLParam(r, "id")
itemId, err := strconv.Atoi(itemIdStr)
if err != nil {
- return err
+ http.Error(w, "invalid item id", http.StatusBadRequest)
+ return
}
i := bs.PayItem(itemId)
- return renderTempl(c, http.StatusOK, view.LineItem(&view.ItemViewModel{
+ if err := renderTempl(w, http.StatusOK, view.LineItem(&view.ItemViewModel{
Id: itemIdStr,
Item: i.Item,
Quantity: strconv.Itoa(i.Quantity),
@@ -368,73 +443,104 @@ func handleItemPay(bs *booking.Service) echo.HandlerFunc {
PaymentStatus: i.PaymentStatus,
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.ID),
- }))
+ })); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handleItemUpdate(bs *booking.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- type updateItem struct {
- Item string `form:"item"`
- PaymentMethod string `form:"paymentMethod"`
- PaymentStatus string `form:"paymentStatus"`
- Id int `param:"id"`
- Quantity int `form:"quantity"`
- Price float64 `form:"price"`
- }
- ui := new(updateItem)
-
- if err := c.Bind(ui); err != nil {
- log.Warn(err)
- return err
+func handleItemUpdate(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
}
- i := bs.UpdateItem(ui.Id, ui.Item, ui.Quantity, ui.Price, ui.PaymentMethod, ui.PaymentStatus)
+ id, err := strconv.Atoi(chi.URLParam(r, "id"))
+ if err != nil {
+ http.Error(w, "invalid item id", http.StatusBadRequest)
+ return
+ }
- return renderTempl(c, http.StatusCreated, view.LineItem(&view.ItemViewModel{
- Id: strconv.Itoa(ui.Id),
+ quantity, err := strconv.Atoi(r.FormValue("quantity"))
+ if err != nil {
+ http.Error(w, "invalid quantity", http.StatusBadRequest)
+ return
+ }
+
+ price := 0.0
+ if v := r.FormValue("price"); v != "" {
+ price, err = strconv.ParseFloat(v, 64)
+ if err != nil {
+ http.Error(w, "invalid price", http.StatusBadRequest)
+ return
+ }
+ }
+
+ i := bs.UpdateItem(id, r.FormValue("item"), quantity, price, r.FormValue("paymentMethod"), r.FormValue("paymentStatus"))
+
+ if err := renderTempl(w, http.StatusCreated, view.LineItem(&view.ItemViewModel{
+ Id: strconv.Itoa(id),
Item: i.Item,
Quantity: strconv.Itoa(i.Quantity),
Price: strconv.FormatFloat(i.Price, 'f', 2, 64),
PaymentStatus: i.PaymentStatus,
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.ID),
- }))
+ })); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handlePaymentUpdate(bs *booking.Service) echo.HandlerFunc {
- type updatePayment struct {
- Id int `param:"id"`
- Amount float64 `form:"amount"`
- PaymentMethod string `form:"paymentMethod"`
- }
- return func(c echo.Context) error {
- up := new(updatePayment)
-
- if err := c.Bind(up); err != nil {
- return fmt.Errorf("could not parse update payment request body: %w", err)
+func handlePaymentUpdate(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
}
- p := bs.UpdatePayment(up.Id, up.Amount, up.PaymentMethod)
+ id, err := strconv.Atoi(chi.URLParam(r, "id"))
+ if err != nil {
+ http.Error(w, "invalid payment id", http.StatusBadRequest)
+ return
+ }
- return renderTempl(c, http.StatusOK, view.PaymentLine(paymentViewModelFromBookingPayment(*p)))
+ amount := 0.0
+ if v := r.FormValue("amount"); v != "" {
+ amount, err = strconv.ParseFloat(v, 64)
+ if err != nil {
+ http.Error(w, "invalid amount", http.StatusBadRequest)
+ return
+ }
+ }
+
+ p := bs.UpdatePayment(id, amount, r.FormValue("paymentMethod"))
+
+ if err := renderTempl(w, http.StatusOK, view.PaymentLine(paymentViewModelFromBookingPayment(*p))); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handleBookingCancel(bs *booking.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- idStr := c.Param("id")
+func handleBookingCancel(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
- return err
+ http.Error(w, "invalid booking id", http.StatusBadRequest)
+ return
}
bs.Cancel(id)
- return renderTempl(c, http.StatusOK, templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
- _, err := io.WriteString(w, " Canceled")
+ component := templ.ComponentFunc(func(ctx context.Context, writer io.Writer) error {
+ _, err := io.WriteString(writer, " Canceled")
return err
- }))
+ })
+
+ if err := renderTempl(w, http.StatusOK, component); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
diff --git a/internal/server/handle_healthz.go b/internal/server/handle_healthz.go
index 31ce366..0c007e6 100644
--- a/internal/server/handle_healthz.go
+++ b/internal/server/handle_healthz.go
@@ -2,12 +2,11 @@ package server
import (
"net/http"
-
- "github.com/labstack/echo/v4"
)
-func handleHealthCheck() echo.HandlerFunc {
- return func(c echo.Context) error {
- return c.String(http.StatusOK, "healthy")
+func handleHealthCheck() http.HandlerFunc {
+ return func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte("healthy"))
}
}
diff --git a/internal/server/handle_payments.go b/internal/server/handle_payments.go
index 9fb19c4..84accd6 100644
--- a/internal/server/handle_payments.go
+++ b/internal/server/handle_payments.go
@@ -4,58 +4,68 @@ import (
"net/http"
"strconv"
- "github.com/labstack/echo/v4"
+ "github.com/go-chi/chi/v5"
u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/service/booking"
"github.com/rjNemo/rentease/internal/view"
)
-func handleCreatePayment(bs *booking.Service) echo.HandlerFunc {
- type CreatePaymentInput struct {
- Amount float64 `form:"amount"`
- PaymentMethod string `form:"paymentMethod"`
- }
-
- return func(c echo.Context) error {
- idStr := c.Param("id")
- id, err := strconv.Atoi(idStr)
- if err != nil {
- return err
+func handleCreatePayment(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
}
- np := new(CreatePaymentInput)
- if err := c.Bind(np); err != nil {
- return err
+ id, err := strconv.Atoi(chi.URLParam(r, "id"))
+ if err != nil {
+ http.Error(w, "invalid booking id", http.StatusBadRequest)
+ return
+ }
+
+ amount := 0.0
+ if v := r.FormValue("amount"); v != "" {
+ amount, err = strconv.ParseFloat(v, 64)
+ if err != nil {
+ http.Error(w, "invalid amount", http.StatusBadRequest)
+ return
+ }
}
b := bs.One(id)
- _, err = bs.CreatePayment(b.ID, np.Amount, np.PaymentMethod)
- if err != nil {
- return err
+ if _, err := bs.CreatePayment(b.ID, amount, r.FormValue("paymentMethod")); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
nb := bs.One(id)
- return renderTempl(c, http.StatusOK, view.PaymentList(
+ component := view.PaymentList(
u.Map(nb.Payments, func(p booking.Payment) *view.PaymentViewModel {
return paymentViewModelFromBookingPayment(p)
}),
- ))
+ )
+
+ if err := renderTempl(w, http.StatusOK, component); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handlePaymentForm(bs *booking.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- idStr := c.Param("id")
- id, err := strconv.Atoi(idStr)
+func handlePaymentForm(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
- return err
+ http.Error(w, "invalid payment id", http.StatusBadRequest)
+ return
}
p := bs.OnePayment(id)
form := view.PaymentForm(paymentViewModelFromBookingPayment(*p))
- return renderTempl(c, http.StatusOK, form)
+ if err := renderTempl(w, http.StatusOK, form); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
diff --git a/internal/server/handle_pdf.go b/internal/server/handle_pdf.go
index 9ec729f..6334eb2 100644
--- a/internal/server/handle_pdf.go
+++ b/internal/server/handle_pdf.go
@@ -2,71 +2,68 @@ package server
import (
"fmt"
- "log"
+ "log/slog"
"net/http"
"strconv"
- "github.com/labstack/echo/v4"
+ "github.com/go-chi/chi/v5"
u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/service/booking"
)
-func handlePdfCreateInvoice(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
- return func(c echo.Context) error {
- idStr := c.Param("id")
+func handlePdfCreateInvoice(bs *booking.Service, hc *config.Host) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
- log.Println(err)
- return err
+ http.Error(w, "invalid booking id", http.StatusBadRequest)
+ return
}
b := bs.One(id)
filePath, err := bs.BuildInvoice(b, hc)
if err != nil {
- log.Println(err)
- return err
+ slog.Error("failed to build invoice", slog.Any("error", err))
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
- return c.File(filePath)
+
+ http.ServeFile(w, r, filePath)
}
}
-func handlePdfCreateReport(bs *booking.Service) echo.HandlerFunc {
- return func(c echo.Context) error {
- period := c.QueryParam("period")
+func handlePdfCreateReport(bs *booking.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ period := r.URL.Query().Get("period")
if !u.Contains([]string{"month", "year"}, period) {
- return &echo.HTTPError{
- Code: http.StatusBadRequest,
- Message: fmt.Sprintf("%q is not a valid period", period),
- }
+ http.Error(w, fmt.Sprintf("%q is not a valid period", period), http.StatusBadRequest)
+ return
}
- monthStr := c.QueryParam("month")
+ monthStr := r.URL.Query().Get("month")
month, err := strconv.Atoi(monthStr)
if err != nil || month < 1 || month > 12 {
- return &echo.HTTPError{
- Code: http.StatusBadRequest,
- Message: fmt.Sprintf("%q is not a valid month", month),
- }
+ http.Error(w, fmt.Sprintf("%q is not a valid month", monthStr), http.StatusBadRequest)
+ return
}
- yearStr := c.QueryParam("year")
+ yearStr := r.URL.Query().Get("year")
year, err := strconv.Atoi(yearStr)
if err != nil {
- return &echo.HTTPError{
- Code: http.StatusBadRequest,
- Message: fmt.Sprintf("%q is not a valid year", year),
- }
+ http.Error(w, fmt.Sprintf("%q is not a valid year", yearStr), http.StatusBadRequest)
+ return
}
report := bs.Report(period, month, year)
filePath, err := bs.BuildReport(report, period, month, year)
if err != nil {
- return err
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
- return c.File(filePath)
+ http.ServeFile(w, r, filePath)
}
}
diff --git a/internal/server/handle_reports.go b/internal/server/handle_reports.go
index 996e907..51409dd 100644
--- a/internal/server/handle_reports.go
+++ b/internal/server/handle_reports.go
@@ -7,7 +7,6 @@ import (
"time"
"github.com/a-h/templ"
- "github.com/labstack/echo/v4"
u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/config"
@@ -16,64 +15,67 @@ import (
"github.com/rjNemo/rentease/internal/view"
)
-func handleReportsPage() echo.HandlerFunc {
- return func(c echo.Context) error {
- monthStr := c.QueryParam("month")
+func handleReportsPage() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ monthStr := r.URL.Query().Get("month")
month, err := strconv.Atoi(monthStr)
if err != nil || month < 1 || month > 12 {
month = int(time.Now().Month())
}
- yearStr := c.QueryParam("year")
- _, err = strconv.Atoi(yearStr)
- if err != nil {
+ yearStr := r.URL.Query().Get("year")
+ if _, err = strconv.Atoi(yearStr); err != nil {
yearStr = time.Now().Format("2006")
}
- return renderTempl(c, http.StatusOK, view.Reports(u.Map(constant.Months, func(m constant.Month) string {
+
+ component := view.Reports(u.Map(constant.Months, func(m constant.Month) string {
return string(m)
- }), month, yearStr))
+ }), month, yearStr)
+
+ if err := renderTempl(w, http.StatusOK, component); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
-func handleReportCompute(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
- return func(c echo.Context) error {
- period := c.FormValue("period")
- if !u.Contains([]string{"month", "year"}, period) {
- return &echo.HTTPError{
- Code: http.StatusBadRequest,
- Message: fmt.Sprintf("%q is not a valid period", period),
- }
+func handleReportCompute(bs *booking.Service, hc *config.Host) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
}
- monthStr := c.FormValue("month")
+ period := r.FormValue("period")
+ if !u.Contains([]string{"month", "year"}, period) {
+ http.Error(w, fmt.Sprintf("%q is not a valid period", period), http.StatusBadRequest)
+ return
+ }
+
+ monthStr := r.FormValue("month")
month, err := strconv.Atoi(monthStr)
if err != nil || month < 1 || month > 12 {
- return &echo.HTTPError{
- Code: http.StatusBadRequest,
- Message: fmt.Sprintf("%q is not a valid month", month),
- }
+ http.Error(w, fmt.Sprintf("%q is not a valid month", monthStr), http.StatusBadRequest)
+ return
}
- yearStr := c.FormValue("year")
+ yearStr := r.FormValue("year")
year, err := strconv.Atoi(yearStr)
if err != nil {
- return &echo.HTTPError{
- Code: http.StatusBadRequest,
- Message: fmt.Sprintf("%q is not a valid year", year),
- }
+ http.Error(w, fmt.Sprintf("%q is not a valid year", yearStr), http.StatusBadRequest)
+ return
}
- r := bs.Report(period, month, year)
+ report := bs.Report(period, month, year)
reportVm := &view.ReportViewModel{
- Total: strconv.FormatFloat(r.Total, 'f', 2, 64),
- PlatformFees: strconv.FormatFloat(r.PlatformFees, 'f', 2, 64),
- Fee: strconv.FormatFloat(r.Fee, 'f', 2, 64),
- Profit: strconv.FormatFloat(r.Profit, 'f', 2, 64),
- CardTotal: strconv.FormatFloat(r.CardTotal, 'f', 2, 64),
- BookingFees: strconv.FormatFloat(r.BookingFees, 'f', 2, 64),
+ Total: strconv.FormatFloat(report.Total, 'f', 2, 64),
+ PlatformFees: strconv.FormatFloat(report.PlatformFees, 'f', 2, 64),
+ Fee: strconv.FormatFloat(report.Fee, 'f', 2, 64),
+ Profit: strconv.FormatFloat(report.Profit, 'f', 2, 64),
+ CardTotal: strconv.FormatFloat(report.CardTotal, 'f', 2, 64),
+ BookingFees: strconv.FormatFloat(report.BookingFees, 'f', 2, 64),
PdfUrl: templ.URL(fmt.Sprintf("%s/pdf?period=%s&month=%d&year=%d", constant.RouteReports, period, month, year)),
- Lines: u.Map(r.Lines, func(l *booking.Line) *view.ReportLine {
+ Lines: u.Map(report.Lines, func(l *booking.Line) *view.ReportLine {
return &view.ReportLine{
Id: l.InvoiceNumber(hc),
Url: templ.SafeURL(fmt.Sprintf("%s/%d", constant.RouteBooking, l.ID)),
@@ -89,6 +91,8 @@ func handleReportCompute(bs *booking.Service, hc *config.Host) echo.HandlerFunc
}),
}
- return renderTempl(c, http.StatusOK, view.ReportSection(reportVm))
+ if err := renderTempl(w, http.StatusOK, view.ReportSection(reportVm)); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
diff --git a/internal/server/handle_stripe_sync.go b/internal/server/handle_stripe_sync.go
index 833dcda..dcaf11c 100644
--- a/internal/server/handle_stripe_sync.go
+++ b/internal/server/handle_stripe_sync.go
@@ -8,8 +8,6 @@ import (
"net/http"
"time"
- "github.com/labstack/echo/v4"
-
"github.com/rjNemo/rentease/internal/service/booking"
)
@@ -22,12 +20,13 @@ type stripeSyncer interface {
SyncStripePayments(ctx context.Context, from, to time.Time) error
}
-func handleStripeSync(bs stripeSyncer) echo.HandlerFunc {
- return func(c echo.Context) error {
+func handleStripeSync(bs stripeSyncer) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
req := new(stripeSyncRequest)
- if err := json.NewDecoder(c.Request().Body).Decode(req); err != nil {
+ if err := json.NewDecoder(r.Body).Decode(req); err != nil {
if !errors.Is(err, io.EOF) {
- return echo.NewHTTPError(http.StatusBadRequest, "invalid request payload")
+ http.Error(w, "invalid request payload", http.StatusBadRequest)
+ return
}
}
@@ -38,7 +37,8 @@ func handleStripeSync(bs stripeSyncer) echo.HandlerFunc {
if req.From != "" {
parsed, err := time.Parse(time.RFC3339, req.From)
if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "invalid 'from' timestamp, expected RFC3339")
+ http.Error(w, "invalid 'from' timestamp, expected RFC3339", http.StatusBadRequest)
+ return
}
from = parsed
}
@@ -46,18 +46,24 @@ func handleStripeSync(bs stripeSyncer) echo.HandlerFunc {
if req.To != "" {
parsed, err := time.Parse(time.RFC3339, req.To)
if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "invalid 'to' timestamp, expected RFC3339")
+ http.Error(w, "invalid 'to' timestamp, expected RFC3339", http.StatusBadRequest)
+ return
}
to = parsed
}
- if err := bs.SyncStripePayments(c.Request().Context(), from, to); err != nil {
+ if err := bs.SyncStripePayments(r.Context(), from, to); err != nil {
if errors.Is(err, booking.ErrStripeClientNotConfigured) {
- return echo.NewHTTPError(http.StatusServiceUnavailable, "stripe client not configured")
+ http.Error(w, "stripe client not configured", http.StatusServiceUnavailable)
+ return
}
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
- return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(map[string]string{"status": "ok"}); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
}
diff --git a/internal/server/handle_stripe_sync_test.go b/internal/server/handle_stripe_sync_test.go
index c93f116..8092a2a 100644
--- a/internal/server/handle_stripe_sync_test.go
+++ b/internal/server/handle_stripe_sync_test.go
@@ -7,8 +7,6 @@ import (
"strings"
"testing"
"time"
-
- "github.com/labstack/echo/v4"
)
type stubStripeSyncer struct {
@@ -31,15 +29,11 @@ func TestHandleStripeSyncSuccess(t *testing.T) {
from := now.Add(-2 * time.Hour).Format(time.RFC3339)
to := now.Format(time.RFC3339)
- e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/api/stripe/sync", strings.NewReader(`{"from":"`+from+`","to":"`+to+`"}`))
- req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
- c := e.NewContext(req, rec)
- if err := handler(c); err != nil {
- t.Fatalf("handler returned error: %v", err)
- }
+ handler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
@@ -54,22 +48,13 @@ func TestHandleStripeSyncInvalidTimestamp(t *testing.T) {
syncer := &stubStripeSyncer{}
handler := handleStripeSync(syncer)
- e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/api/stripe/sync", strings.NewReader(`{"from":"not-a-date"}`))
- req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
- c := e.NewContext(req, rec)
- err := handler(c)
- if err == nil {
- t.Fatal("expected error for invalid timestamp")
- }
+ handler(rec, req)
- httpErr, ok := err.(*echo.HTTPError)
- if !ok {
- t.Fatalf("expected HTTPError, got %T", err)
- }
- if httpErr.Code != http.StatusBadRequest {
- t.Fatalf("expected status 400, got %d", httpErr.Code)
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("expected status 400, got %d", rec.Code)
}
}
diff --git a/internal/server/handle_stripe_webhook.go b/internal/server/handle_stripe_webhook.go
index b8fe9fc..f529ce6 100644
--- a/internal/server/handle_stripe_webhook.go
+++ b/internal/server/handle_stripe_webhook.go
@@ -6,7 +6,6 @@ import (
"io"
"net/http"
- "github.com/labstack/echo/v4"
"github.com/stripe/stripe-go/v83"
"github.com/stripe/stripe-go/v83/webhook"
)
@@ -16,50 +15,60 @@ type stripeEventService interface {
HandleChargeRefunded(ctx context.Context, ch *stripe.Charge) error
}
-func handleStripeWebhook(bs stripeEventService, secret string) echo.HandlerFunc {
- return func(c echo.Context) error {
+var constructEvent = webhook.ConstructEvent
+
+func handleStripeWebhook(bs stripeEventService, secret string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
if secret == "" {
- return echo.NewHTTPError(http.StatusServiceUnavailable, "stripe webhook secret not configured")
+ http.Error(w, "stripe webhook secret not configured", http.StatusServiceUnavailable)
+ return
}
- payload, err := io.ReadAll(c.Request().Body)
+ payload, err := io.ReadAll(r.Body)
if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "unable to read request body")
+ http.Error(w, "unable to read request body", http.StatusBadRequest)
+ return
}
- sig := c.Request().Header.Get("Stripe-Signature")
+ sig := r.Header.Get("Stripe-Signature")
if sig == "" {
- return echo.NewHTTPError(http.StatusBadRequest, "missing Stripe-Signature header")
+ http.Error(w, "missing Stripe-Signature header", http.StatusBadRequest)
+ return
}
- event, err := webhook.ConstructEvent(payload, sig, secret)
+ event, err := constructEvent(payload, sig, secret)
if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "invalid webhook signature")
+ http.Error(w, "invalid webhook signature", http.StatusBadRequest)
+ return
}
switch event.Type {
case stripe.EventTypePaymentIntentSucceeded:
var pi stripe.PaymentIntent
if err := json.Unmarshal(event.Data.Raw, &pi); err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "invalid payment intent payload")
+ http.Error(w, "invalid payment intent payload", http.StatusBadRequest)
+ return
}
- if err := bs.HandlePaymentIntentSucceeded(c.Request().Context(), &pi); err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ if err := bs.HandlePaymentIntentSucceeded(r.Context(), &pi); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
case stripe.EventTypeChargeRefunded:
var ch stripe.Charge
if err := json.Unmarshal(event.Data.Raw, &ch); err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "invalid charge payload")
+ http.Error(w, "invalid charge payload", http.StatusBadRequest)
+ return
}
- if err := bs.HandleChargeRefunded(c.Request().Context(), &ch); err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ if err := bs.HandleChargeRefunded(r.Context(), &ch); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
default:
// Acknowledge events we don't actively process.
}
- return c.NoContent(http.StatusOK)
+ w.WriteHeader(http.StatusOK)
}
}
diff --git a/internal/server/handle_stripe_webhook_test.go b/internal/server/handle_stripe_webhook_test.go
index 4831c4a..8472bda 100644
--- a/internal/server/handle_stripe_webhook_test.go
+++ b/internal/server/handle_stripe_webhook_test.go
@@ -2,18 +2,13 @@ package server
import (
"context"
- "encoding/hex"
"encoding/json"
- "fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
- "time"
- "github.com/labstack/echo/v4"
"github.com/stripe/stripe-go/v83"
- "github.com/stripe/stripe-go/v83/webhook"
)
type stubStripeEventService struct {
@@ -56,26 +51,18 @@ func TestHandleStripeWebhookPaymentIntent(t *testing.T) {
t.Fatalf("failed to marshal payload: %v", err)
}
- ts := time.Now()
- sig := webhook.ComputeSignature(ts, payloadBytes, secret)
- sigHeader := fmt.Sprintf("t=%d,v1=%s", ts.Unix(), hex.EncodeToString(sig))
- if _, err := webhook.ConstructEvent(payloadBytes, sigHeader, secret); err != nil {
- t.Fatalf("signature validation failed in setup: %v", err)
- }
-
- e := echo.New()
- req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader(string(payloadBytes)))
- req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
- req.Header.Set("Stripe-Signature", sigHeader)
- rec := httptest.NewRecorder()
- c := e.NewContext(req, rec)
+ restore := stubConstructEvent(stripe.EventTypePaymentIntentSucceeded, payloadBytes)
+ defer restore()
service := &stubStripeEventService{}
handler := handleStripeWebhook(service, secret)
- if err := handler(c); err != nil {
- t.Fatalf("handler returned error: %v", err)
- }
+ req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader(string(payloadBytes)))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Stripe-Signature", "test")
+ rec := httptest.NewRecorder()
+
+ handler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
@@ -105,26 +92,18 @@ func TestHandleStripeWebhookChargeRefunded(t *testing.T) {
}
payloadBytes, _ := json.Marshal(payload)
- ts := time.Now()
- sig := webhook.ComputeSignature(ts, payloadBytes, secret)
- sigHeader := fmt.Sprintf("t=%d,v1=%s", ts.Unix(), hex.EncodeToString(sig))
- if _, err := webhook.ConstructEvent(payloadBytes, sigHeader, secret); err != nil {
- t.Fatalf("signature validation failed in setup: %v", err)
- }
-
- e := echo.New()
- req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader(string(payloadBytes)))
- req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
- req.Header.Set("Stripe-Signature", sigHeader)
- rec := httptest.NewRecorder()
- c := e.NewContext(req, rec)
+ restore := stubConstructEvent(stripe.EventTypeChargeRefunded, payloadBytes)
+ defer restore()
service := &stubStripeEventService{}
handler := handleStripeWebhook(service, secret)
- if err := handler(c); err != nil {
- t.Fatalf("handler returned error: %v", err)
- }
+ req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader(string(payloadBytes)))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Stripe-Signature", "test")
+ rec := httptest.NewRecorder()
+
+ handler(rec, req)
if !service.chargeCalled {
t.Fatalf("expected charge handler to be called")
@@ -132,19 +111,24 @@ func TestHandleStripeWebhookChargeRefunded(t *testing.T) {
}
func TestHandleStripeWebhookInvalidSignature(t *testing.T) {
- e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader("{}"))
req.Header.Set("Stripe-Signature", "invalid")
rec := httptest.NewRecorder()
- c := e.NewContext(req, rec)
handler := handleStripeWebhook(&stubStripeEventService{}, "secret")
- err := handler(c)
- if err == nil {
- t.Fatal("expected error for invalid signature")
- }
+ handler(rec, req)
- if httpErr, ok := err.(*echo.HTTPError); !ok || httpErr.Code != http.StatusBadRequest {
- t.Fatalf("expected 400 HTTP error, got %v", err)
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("expected 400 status, got %d", rec.Code)
}
}
+
+func stubConstructEvent(eventType stripe.EventType, payload []byte) func() {
+ original := constructEvent
+ constructEvent = func(_ []byte, _ string, _ string) (stripe.Event, error) {
+ event := stripe.Event{Type: eventType}
+ event.Data = &stripe.EventData{Raw: payload}
+ return event, nil
+ }
+ return func() { constructEvent = original }
+}
diff --git a/internal/server/helper.go b/internal/server/helper.go
index 78c7e8e..6e4fe70 100644
--- a/internal/server/helper.go
+++ b/internal/server/helper.go
@@ -3,16 +3,16 @@ package server
import (
"context"
"log"
+ "net/http"
"github.com/a-h/templ"
- "github.com/labstack/echo/v4"
)
-func renderTempl(c echo.Context, status int, t templ.Component) error {
- c.Response().Writer.WriteHeader(status)
+func renderTempl(w http.ResponseWriter, status int, t templ.Component) error {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(status)
- err := t.Render(context.Background(), c.Response().Writer)
- if err != nil {
+ if err := t.Render(context.Background(), w); err != nil {
log.Printf("failed to render response template %s", err)
return err
}
@@ -20,29 +20,16 @@ func renderTempl(c echo.Context, status int, t templ.Component) error {
return nil
}
-func hxRedirect(c echo.Context, statusCode int, url string) error {
- c.Response().Header().Add("HX-Redirect", url)
- return c.NoContent(statusCode)
+func hxRedirect(w http.ResponseWriter, statusCode int, url string) error {
+ w.Header().Add("HX-Redirect", url)
+ w.WriteHeader(statusCode)
+ return nil
}
-func hxRequest(c echo.Context) bool {
- header, ok := c.Request().Header["Hx-Request"]
- if !ok {
- return false
- }
- if header[0] != "true" {
- return false
- }
- return true
+func hxRequest(r *http.Request) bool {
+ return r.Header.Get("Hx-Request") == "true"
}
-func hxBoosted(c echo.Context) bool {
- header, ok := c.Request().Header["Hx-Boosted"]
- if !ok {
- return false
- }
- if header[0] != "true" {
- return false
- }
- return true
+func hxBoosted(r *http.Request) bool {
+ return r.Header.Get("Hx-Boosted") == "true"
}
diff --git a/internal/server/middleware.go b/internal/server/middleware.go
index d09d1cd..3efd7fd 100644
--- a/internal/server/middleware.go
+++ b/internal/server/middleware.go
@@ -5,7 +5,6 @@ import (
"net/http"
"strings"
- "github.com/labstack/echo/v4"
"github.com/rjNemo/underscore"
)
@@ -15,31 +14,34 @@ const (
// CachingMiddleware adds caching headers.
//
-// ttl is the max age of the cache in seconds. If ttl is 0, the default value wil be used.
-func CachingMiddleware(ttl int, fileTypes ...string) echo.MiddlewareFunc {
+// ttl is the max age of the cache in seconds. If ttl is 0, the default value will be used.
+func CachingMiddleware(ttl int, fileTypes ...string) func(http.Handler) http.Handler {
if ttl == 0 {
ttl = defaultTTL
}
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
+
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
shouldCache := underscore.Any(fileTypes, func(v string) bool {
- return strings.HasSuffix(c.Request().RequestURI, fmt.Sprintf(".%s", v))
+ return strings.HasSuffix(r.URL.Path, fmt.Sprintf(".%s", v))
})
if shouldCache {
- s := strings.Split(c.Request().RequestURI, "/")
- etag := s[len(s)-1]
+ segments := strings.Split(r.URL.Path, "/")
+ etag := segments[len(segments)-1]
- c.Response().Header().Set("Etag", etag)
- c.Response().Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", ttl))
- if match := c.Request().Header.Get("If-None-Match"); match != "" {
+ w.Header().Set("Etag", etag)
+ w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", ttl))
+
+ if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
- return c.NoContent(http.StatusNotModified)
+ w.WriteHeader(http.StatusNotModified)
+ return
}
}
}
- return next(c)
- }
+ next.ServeHTTP(w, r)
+ })
}
}
diff --git a/internal/server/routes.go b/internal/server/routes.go
index e8b6671..6969060 100644
--- a/internal/server/routes.go
+++ b/internal/server/routes.go
@@ -1,50 +1,63 @@
package server
import (
- "github.com/labstack/echo/v4"
- "github.com/labstack/echo/v4/middleware"
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+
+ "github.com/rjNemo/rentease/internal/service/auth"
)
-func (s Server) MountHandlers() {
- s.Router.GET("/healthz", handleHealthCheck())
- s.Router.GET("/", handleLoginPage())
- s.Router.POST("/", handleLogin(s.as))
- s.Router.POST("/webhooks/stripe", handleStripeWebhook(s.bs, s.stripeWebhookSecret))
+func (s *Server) MountHandlers() {
+ s.Router.Get("/healthz", handleHealthCheck())
+ s.Router.Get("/", handleLoginPage())
+ s.Router.Post("/", handleLogin(s.as))
+ s.Router.Post("/webhooks/stripe", handleStripeWebhook(s.bs, s.stripeWebhookSecret))
- api := s.Router.Group("/api")
- api.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
- KeyLookup: "header:api-key",
- Validator: func(key string, c echo.Context) (bool, error) {
- return s.as.ValidateAPIKey(key), nil
- },
- }))
- api.POST("/sync", handleSync(s.bs))
- api.GET("/bookings", handleBookingList(s.bs))
- api.POST("/bookings", handleCreateBooking(s.bs))
- api.POST("/stripe/sync", handleStripeSync(s.bs))
+ s.Router.Route("/api", func(r chi.Router) {
+ r.Use(apiKeyMiddleware(s.as))
+ r.Post("/sync", handleSync(s.bs))
+ r.Get("/bookings", handleBookingList(s.bs))
+ r.Post("/bookings", handleCreateBooking(s.bs))
+ r.Post("/stripe/sync", handleStripeSync(s.bs))
+ })
- private := s.Router.Group("")
- private.Use(MakeAuthMiddleware(s.as))
+ s.Router.Group(func(r chi.Router) {
+ r.Use(MakeAuthMiddleware(s.as))
- private.GET("/bookings", handleBookingListPage(s.bs, s.hc))
- private.GET("/bookings/new", handleBookingCreatePage(s.hc))
- private.POST("/bookings/new", handleBookingCreate(s.bs))
- private.GET("/bookings/:id", handleBookingPage(s.bs, s.hc))
- private.POST("/bookings/:id/stripe/payment-link", handleBookingStripePaymentLink(s.bs))
- private.PUT("/bookings/:id", handleBookingUpdate(s.bs, s.hc))
- private.PATCH("/bookings/:id/cancel", handleBookingCancel(s.bs))
- private.POST("/bookings/:id/items", handleCreateItem(s.bs, s.hc))
- private.GET("/bookings/pdf/:id", handlePdfCreateInvoice(s.bs, s.hc))
+ r.Get("/bookings", handleBookingListPage(s.bs, s.hc))
+ r.Get("/bookings/new", handleBookingCreatePage(s.hc))
+ r.Post("/bookings/new", handleBookingCreate(s.bs))
+ r.Get("/bookings/{id}", handleBookingPage(s.bs, s.hc))
+ r.Post("/bookings/{id}/stripe/payment-link", handleBookingStripePaymentLink(s.bs))
+ r.Put("/bookings/{id}", handleBookingUpdate(s.bs, s.hc))
+ r.Patch("/bookings/{id}/cancel", handleBookingCancel(s.bs))
+ r.Post("/bookings/{id}/items", handleCreateItem(s.bs, s.hc))
+ r.Get("/bookings/pdf/{id}", handlePdfCreateInvoice(s.bs, s.hc))
- private.POST("/items/:id", handleItemPay(s.bs))
- private.PUT("/items/:id", handleItemUpdate(s.bs))
- private.GET("/items/:id", handleLineItemForm(s.bs))
+ r.Post("/items/{id}", handleItemPay(s.bs))
+ r.Put("/items/{id}", handleItemUpdate(s.bs))
+ r.Get("/items/{id}", handleLineItemForm(s.bs))
- private.GET("/reports", handleReportsPage())
- private.GET("/reports/do", handleReportCompute(s.bs, s.hc))
- private.GET("/reports/pdf", handlePdfCreateReport(s.bs))
+ r.Get("/reports", handleReportsPage())
+ r.Get("/reports/do", handleReportCompute(s.bs, s.hc))
+ r.Get("/reports/pdf", handlePdfCreateReport(s.bs))
- private.POST("/payments/:id", handleCreatePayment(s.bs))
- private.PUT("/payments/:id", handlePaymentUpdate(s.bs))
- private.GET("/payments/:id", handlePaymentForm(s.bs))
+ r.Post("/payments/{id}", handleCreatePayment(s.bs))
+ r.Put("/payments/{id}", handlePaymentUpdate(s.bs))
+ r.Get("/payments/{id}", handlePaymentForm(s.bs))
+ })
+}
+
+func apiKeyMiddleware(as *auth.Service) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !as.ValidateAPIKey(r.Header.Get("api-key")) {
+ http.Error(w, "invalid api key", http.StatusUnauthorized)
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+ }
}
diff --git a/internal/server/server.go b/internal/server/server.go
index 0346f2e..78b7ace 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -5,17 +5,17 @@ import (
"embed"
"errors"
"fmt"
+ "io/fs"
+ "log/slog"
"net/http"
"os"
"os/signal"
"time"
- "github.com/getsentry/sentry-go"
- sentryecho "github.com/getsentry/sentry-go/echo"
- "github.com/gorilla/sessions"
- "github.com/labstack/echo-contrib/session"
- "github.com/labstack/echo/v4"
- "github.com/labstack/echo/v4/middleware"
+ sentryhttp "github.com/getsentry/sentry-go/http"
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
+ "github.com/go-chi/cors"
"github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/service/auth"
@@ -23,7 +23,8 @@ import (
)
type Server struct {
- Router *echo.Echo
+ Router *chi.Mux
+ httpServer *http.Server
bs *booking.Service
as *auth.Service
hc *config.Host
@@ -34,14 +35,18 @@ type Server struct {
func New(bs *booking.Service, as *auth.Service, hc *config.Host, opts ...Option) (*Server, error) {
option := new(options)
for _, opt := range opts {
- err := opt(option)
- if err != nil {
+ if err := opt(option); err != nil {
return nil, err
}
}
+ router, err := NewRouter(*option.fs, *option.debug, option.origins)
+ if err != nil {
+ return nil, err
+ }
+
s := &Server{
- Router: NewRouter(*option.fs, *option.debug, *option.secretKey, option.origins),
+ Router: router,
bs: bs,
as: as,
hc: hc,
@@ -57,50 +62,73 @@ func New(bs *booking.Service, as *auth.Service, hc *config.Host, opts ...Option)
return s, nil
}
-func (s Server) Start(c context.Context) {
+func (s *Server) Start(ctx context.Context) {
+ s.httpServer = &http.Server{
+ Addr: s.addr,
+ Handler: s.Router,
+ }
+
go func() {
- if err := s.Router.Start(s.addr); err != nil && !errors.Is(err, http.ErrServerClosed) {
- s.Router.Logger.Fatalf("shutting down the server: %s", err)
+ if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ slog.Error("shutting down the server", slog.Any("error", err))
+ os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
- <-quit
- ctx, cancel := context.WithTimeout(c, 10*time.Second)
+ defer signal.Stop(quit)
+
+ select {
+ case <-quit:
+ case <-ctx.Done():
+ }
+
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
- if err := s.Router.Shutdown(ctx); err != nil {
- s.Router.Logger.Fatal(err)
+
+ if err := s.httpServer.Shutdown(shutdownCtx); err != nil {
+ slog.Error("server shutdown failed", slog.Any("error", err))
+ os.Exit(1)
}
}
-func NewRouter(fs embed.FS, debug bool, secret string, origins []string) *echo.Echo {
- e := echo.New()
- // config
- e.HideBanner = !debug
- e.Debug = debug
- // middlewares
- e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
- Format: "${time_rfc3339} [${method}: ${status}] ${uri}; ip=${remote_ip}; ${latency_human}; ${user_agent}\n",
+func NewRouter(filesystem embed.FS, debug bool, origins []string) (*chi.Mux, error) {
+ r := chi.NewRouter()
+ _ = debug
+
+ r.Use(middleware.RequestID)
+ r.Use(middleware.RealIP)
+ r.Use(middleware.Logger)
+ r.Use(middleware.Recoverer)
+ r.Use(middleware.Compress(5))
+ r.Use(CachingMiddleware(0, "js", "css", "png", "ico"))
+
+ if len(origins) == 0 {
+ origins = []string{"*"}
+ }
+
+ r.Use(cors.Handler(cors.Options{
+ AllowedOrigins: origins,
+ AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodOptions},
+ AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "HX-Request", "HX-Boosted", "api-key"},
+ ExposedHeaders: []string{"HX-Redirect"},
+ AllowCredentials: true,
}))
- e.Use(middleware.Recover())
- e.Use(middleware.Secure())
- e.Use(middleware.Gzip())
- e.Use(middleware.CORSWithConfig(middleware.CORSConfig{AllowOrigins: origins}))
- e.Use(sentryecho.New(sentryecho.Options{}))
- e.Use(SentryTracingMiddleware)
- e.Use(CachingMiddleware(0, "js", "css", "png", "ico"))
- e.Use(session.Middleware(sessions.NewCookieStore([]byte(secret))))
- // static assets
- e.StaticFS("/static", echo.MustSubFS(fs, "assets"))
- return e
-}
+ sentryHandler := sentryhttp.New(sentryhttp.Options{
+ Repanic: true,
+ })
+ r.Use(sentryHandler.Handle)
+ r.Use(SentryTracingMiddleware)
-func captureError(c echo.Context, err error) {
- if hub := sentryecho.GetHubFromContext(c); hub != nil {
- hub.WithScope(func(s *sentry.Scope) {
- hub.CaptureMessage(err.Error())
- })
+ assetsFS, err := fs.Sub(filesystem, "assets")
+ if err != nil {
+ return nil, fmt.Errorf("failed to load static assets: %w", err)
}
+
+ fileServer := http.StripPrefix("/static/", http.FileServer(http.FS(assetsFS)))
+ r.Handle("/static/*", fileServer)
+
+ return r, nil
}
diff --git a/internal/server/tracing.go b/internal/server/tracing.go
index 9ef5b86..f870369 100644
--- a/internal/server/tracing.go
+++ b/internal/server/tracing.go
@@ -2,34 +2,34 @@ package server
import (
"fmt"
+ "net/http"
"github.com/getsentry/sentry-go"
- "github.com/labstack/echo/v4"
)
-func SentryTracingMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- ctx := c.Request().Context()
+func SentryTracingMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
- // Check the concurrency guide for more details: https://docs.sentry.io/platforms/go/concurrency/
hub = sentry.CurrentHub().Clone()
ctx = sentry.SetHubOnContext(ctx, hub)
+ r = r.WithContext(ctx)
}
options := []sentry.SpanOption{
- // Set the OP based on values from https://develop.sentry.dev/sdk/performance/span-operations/
sentry.WithOpName("http.server"),
- sentry.ContinueFromRequest(c.Request()),
+ sentry.ContinueFromRequest(r),
sentry.WithTransactionSource(sentry.SourceURL),
}
- transaction := sentry.StartTransaction(ctx,
- fmt.Sprintf("%s %s", c.Request().Method, c.Request().URL.Path),
+ transaction := sentry.StartTransaction(
+ ctx,
+ fmt.Sprintf("%s %s", r.Method, r.URL.Path),
options...,
)
-
defer transaction.Finish()
- return next(c)
- }
+
+ next.ServeHTTP(w, r.WithContext(transaction.Context()))
+ })
}
diff --git a/internal/service/auth/service.go b/internal/service/auth/service.go
index b91e6ee..5a10cf9 100644
--- a/internal/service/auth/service.go
+++ b/internal/service/auth/service.go
@@ -2,10 +2,9 @@ package auth
import (
"errors"
+ "net/http"
"github.com/gorilla/sessions"
- "github.com/labstack/echo-contrib/session"
- "github.com/labstack/echo/v4"
"github.com/rjNemo/rentease/internal/constant"
)
@@ -20,6 +19,7 @@ type Service struct {
admin string
adminSecret string
apiKey string
+ store *sessions.CookieStore
}
func NewService(secret, admin, adminSecret, apiKey string) (*Service, error) {
@@ -27,11 +27,19 @@ func NewService(secret, admin, adminSecret, apiKey string) (*Service, error) {
return nil, errors.New("error building Auth service. Verify your env variables")
}
+ store := sessions.NewCookieStore([]byte(secret))
+ store.Options = &sessions.Options{
+ Path: constant.RouteLogin,
+ MaxAge: sessionAge,
+ HttpOnly: true,
+ }
+
return &Service{
- secret,
- admin,
- adminSecret,
- apiKey,
+ secret: secret,
+ admin: admin,
+ adminSecret: adminSecret,
+ apiKey: apiKey,
+ store: store,
}, nil
}
@@ -43,33 +51,27 @@ func (as *Service) ValidateAPIKey(key string) bool {
return key == as.apiKey
}
-func (as *Service) getSession(c echo.Context) (*sessions.Session, error) {
- sess, err := session.Get(sessionName, c)
+func (as *Service) getSession(r *http.Request) (*sessions.Session, error) {
+ sess, err := as.store.Get(r, sessionName)
if err != nil {
return nil, err
}
- sess.Options = &sessions.Options{
- Path: constant.RouteLogin,
- MaxAge: sessionAge,
- HttpOnly: true,
- }
-
return sess, nil
}
-func (as *Service) Authenticate(c echo.Context, key string) error {
- sess, err := as.getSession(c)
+func (as *Service) Authenticate(w http.ResponseWriter, r *http.Request, key string) error {
+ sess, err := as.getSession(r)
if err != nil {
return err
}
sess.Values["user"] = key
- return sess.Save(c.Request(), c.Response())
+ return sess.Save(r, w)
}
-func (as *Service) Authenticated(c echo.Context) bool {
- sess, err := as.getSession(c)
+func (as *Service) Authenticated(r *http.Request) bool {
+ sess, err := as.getSession(r)
if err != nil {
return false
}