mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-06 02:36:49 +00:00
feat(deps): migrate from Echo to Chi, update Stripe/Sentry (#49)
Some checks failed
CI / checks (push) Has been cancelled
Some checks failed
CI / checks (push) Has been cancelled
This commit is contained in:
parent
91a9a74750
commit
4bd47dc6e9
21 changed files with 731 additions and 602 deletions
16
go.mod
16
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
|
||||
)
|
||||
|
|
|
|||
33
go.sum
33
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=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, " <span>Canceled</span>")
|
||||
component := templ.ComponentFunc(func(ctx context.Context, writer io.Writer) error {
|
||||
_, err := io.WriteString(writer, " <span>Canceled</span>")
|
||||
return err
|
||||
}))
|
||||
})
|
||||
|
||||
if err := renderTempl(w, http.StatusOK, component); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue