feat(deps): migrate from Echo to Chi, update Stripe/Sentry (#49)
Some checks failed
CI / checks (push) Has been cancelled

This commit is contained in:
Ruidy 2025-11-02 21:45:37 +01:00 committed by GitHub
parent 91a9a74750
commit 4bd47dc6e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 731 additions and 602 deletions

16
go.mod
View file

@ -4,15 +4,13 @@ go 1.25.3
require ( require (
github.com/a-h/templ v0.3.960 github.com/a-h/templ v0.3.960
github.com/getsentry/sentry-go v0.36.0 github.com/getsentry/sentry-go v0.36.2
github.com/getsentry/sentry-go/echo v0.36.0 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/gorilla/sessions v1.4.0
github.com/joho/godotenv v1.5.1 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/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/driver/postgres v1.6.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.31.0
) )
@ -26,7 +24,6 @@ require (
) )
require ( require (
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/openai/openai-go v1.12.0
github.com/sethvargo/go-envconfig v1.3.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/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.14.0 // indirect
) )

33
go.sum
View file

@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.2 h1:uhuxRPTrUy0dnSzTd0LrYXlBYygLkKY0hhlG5LXarzM=
github.com/getsentry/sentry-go v0.36.0/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c= github.com/getsentry/sentry-go v0.36.2/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c=
github.com/getsentry/sentry-go/echo v0.36.0 h1:PimJIxiH2O/nS+jegFLxx52RMpVY2ciAIvVkk8miVeM= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/getsentry/sentry-go/echo v0.36.0/go.mod h1:Z4Q44b9OWBO18lFcC1yfCqOVex00nz2WPSH1AuUUC5I= 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 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 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/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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0=
github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= 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= 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.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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.1.0 h1:h6Wi8+dSUCmIdXDWObs1AirP9tQGWWI/4xP5oE5G6uQ=
github.com/stripe/stripe-go/v83 v83.0.1/go.mod h1:nRyDcLrJtwPPQUnKAFs9Bt1NnQvNhNiF6V19XHmPISE= 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.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 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/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 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= 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/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 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 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/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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -41,9 +41,6 @@ func JobStripePaymentSync() error {
store := booking.NewPgStore(db) store := booking.NewPgStore(db)
opts := []stripeclient.Option{} opts := []stripeclient.Option{}
if cfg.StripeConnectAccount != "" {
opts = append(opts, stripeclient.WithAccount(cfg.StripeConnectAccount))
}
client, err := stripeclient.New(cfg.StripeSecretKey, opts...) client, err := stripeclient.New(cfg.StripeSecretKey, opts...)
if err != nil { if err != nil {

View file

@ -3,23 +3,24 @@ package server
import ( import (
"net/http" "net/http"
"github.com/labstack/echo/v4"
"github.com/rjNemo/rentease/internal/constant" "github.com/rjNemo/rentease/internal/constant"
"github.com/rjNemo/rentease/internal/service/auth" "github.com/rjNemo/rentease/internal/service/auth"
) )
func MakeAuthMiddleware(as *auth.Service) echo.MiddlewareFunc { func MakeAuthMiddleware(as *auth.Service) func(http.Handler) http.Handler {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next http.Handler) http.Handler {
return func(c echo.Context) error { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if c.Request().RequestURI == constant.RouteLogin { if r.URL.Path == constant.RouteLogin {
return next(c) next.ServeHTTP(w, r)
return
} }
if !as.Authenticated(c) { if !as.Authenticated(r) {
return c.Redirect(http.StatusSeeOther, constant.RouteLogin) http.Redirect(w, r, constant.RouteLogin, http.StatusSeeOther)
return
} }
return next(c)
} next.ServeHTTP(w, r)
})
} }
} }

View file

@ -4,68 +4,82 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
"github.com/rjNemo/rentease/internal/service/booking" "github.com/rjNemo/rentease/internal/service/booking"
) )
func handleSync(bs *booking.Service) echo.HandlerFunc { func handleSync(bs *booking.Service) http.HandlerFunc {
type BookingInfo struct { type BookingInfo struct {
Content string `json:"content"` Content string `json:"content"`
} }
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
log.Info("received booking sync request from booking") slog.Info("received booking sync request from booking")
x := c.Request().Body body, err := io.ReadAll(r.Body)
body, err := io.ReadAll(x)
if err != nil { 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) bookingInfo := new(BookingInfo)
err = json.Unmarshal(body, bookingInfo) if err := json.Unmarshal(body, bookingInfo); err != nil {
if err != nil { http.Error(w, fmt.Sprintf("error unmarshalling JSON: %s", err), http.StatusInternalServerError)
return c.String(http.StatusInternalServerError, fmt.Sprintf("error unmarshalling JSON: %s", err)) return
} }
b, err := bs.ParseFromAPI(bookingInfo.Content) b, err := bs.ParseFromAPI(bookingInfo.Content)
if err != nil { 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 { type BookingInfo struct {
Content string `json:"content"` Content string `json:"content"`
} }
return func(c echo.Context) error {
log.Info("received booking sync request from booking") return func(w http.ResponseWriter, r *http.Request) {
x := c.Request().Body slog.Info("received booking sync request from booking")
body, err := io.ReadAll(x) body, err := io.ReadAll(r.Body)
if err != nil { 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) bookingInfo := new(BookingInfo)
err = json.Unmarshal(body, bookingInfo) if err := json.Unmarshal(body, bookingInfo); err != nil {
if err != nil { http.Error(w, fmt.Sprintf("error unmarshalling JSON: %s", err), http.StatusInternalServerError)
return c.String(http.StatusInternalServerError, fmt.Sprintf("error unmarshalling JSON: %s", err)) return
} }
b, err := bs.ParseFromAPI(bookingInfo.Content) b, err := bs.ParseFromAPI(bookingInfo.Content)
if err != nil { 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))
}
} }
} }

View file

@ -3,23 +3,28 @@ package server
import ( import (
"net/http" "net/http"
"github.com/labstack/echo/v4"
"github.com/rjNemo/rentease/internal/constant" "github.com/rjNemo/rentease/internal/constant"
"github.com/rjNemo/rentease/internal/service/auth" "github.com/rjNemo/rentease/internal/service/auth"
"github.com/rjNemo/rentease/internal/view" "github.com/rjNemo/rentease/internal/view"
) )
func handleLoginPage() echo.HandlerFunc { func handleLoginPage() http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
return renderTempl(c, http.StatusOK, view.Login(view.LoginFormViewModel{})) 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 { func handleLogin(as *auth.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
email := c.FormValue("email") if err := r.ParseForm(); err != nil {
password := c.FormValue("password") http.Error(w, err.Error(), http.StatusBadRequest)
return
}
email := r.FormValue("email")
password := r.FormValue("password")
if !as.ValidCredentials(email, password) { if !as.ValidCredentials(email, password) {
lfvm := view.LoginFormViewModel{ lfvm := view.LoginFormViewModel{
@ -28,14 +33,19 @@ func handleLogin(as *auth.Service) echo.HandlerFunc {
Errors: make(map[string]string), Errors: make(map[string]string),
} }
lfvm.Errors["credentials"] = "invalid credentials" 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 := as.Authenticate(w, r, "foo"); err != nil {
if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError)
return err 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)
}
} }
} }

View file

@ -1,7 +1,9 @@
package server package server
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -11,8 +13,7 @@ import (
"time" "time"
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/labstack/echo/v4" "github.com/go-chi/chi/v5"
"github.com/labstack/gommon/log"
u "github.com/rjNemo/underscore" u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/config"
@ -22,9 +23,9 @@ import (
myTime "github.com/rjNemo/rentease/pkg/time" myTime "github.com/rjNemo/rentease/pkg/time"
) )
func handleBookingListPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc { func handleBookingListPage(bs *booking.Service, hc *config.Host) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
search := c.FormValue("search") search := r.URL.Query().Get("search")
var bookings []*booking.Line var bookings []*booking.Line
if search != "" { if search != "" {
@ -48,10 +49,16 @@ func handleBookingListPage(bs *booking.Service, hc *config.Host) echo.HandlerFun
} }
}) })
if hxRequest(c) && !hxBoosted(c) { var err error
return renderTempl(c, http.StatusOK, view.BookingLines(bvm)) switch {
} else { case hxRequest(r) && !hxBoosted(r):
return renderTempl(c, http.StatusOK, view.ListBookings(bvm)) 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 { func handleBookingList(bs *booking.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
search := c.FormValue("search") search := r.URL.Query().Get("search")
var bookings []*booking.Line var bookings []*booking.Line
if search != "" { if search != "" {
@ -81,57 +88,77 @@ func handleBookingList(bs *booking.Service) echo.HandlerFunc {
bookings = bs.All() 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 { func handleBookingCreatePage(hc *config.Host) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
return renderTempl(c, http.StatusOK, view.NewBooking(u.Map(hc.Platforms, func(p config.Platform) string { if err := renderTempl(w, http.StatusOK, view.NewBooking(u.Map(hc.Platforms, func(p config.Platform) string {
return string(p) return string(p)
}))) }))); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} }
} }
func handleBookingCreate(bs *booking.Service) echo.HandlerFunc { func handleBookingCreate(bs *booking.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
type NewBooking struct { if err := r.ParseForm(); err != nil {
From time.Time `json:"from"` http.Error(w, err.Error(), http.StatusBadRequest)
To time.Time `json:"to"` return
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"`
} }
nb := new(NewBooking)
err := c.Bind(nb) customerNumber, err := strconv.Atoi(r.FormValue("customer_number"))
if err != nil { if err != nil {
log.Warn(err) http.Error(w, "invalid customer number", http.StatusBadRequest)
return err return
} }
ts, _ := myTime.ParseFromForm(c.FormValue("from")) platformFees := 0.0
nb.From = ts if v := r.FormValue("platform_fees"); v != "" {
ts, _ = myTime.ParseFromForm(c.FormValue("to")) platformFees, err = strconv.ParseFloat(v, 64)
nb.To = ts if err != nil {
http.Error(w, "invalid platform fees", http.StatusBadRequest)
if *nb.ExternalId == "" { return
nb.ExternalId = nil }
} }
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 { func handleBookingPage(bs *booking.Service, hc *config.Host) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
idStr := c.Param("id") idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
return err http.Error(w, "invalid booking id", http.StatusBadRequest)
return
} }
b := bs.One(id) b := bs.One(id)
@ -189,67 +216,99 @@ func handleBookingPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
), ),
PaymentMethods: hc.PaymentMethods, 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 { func handleBookingStripePaymentLink(bs *booking.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
idStr := c.Param("id") idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { 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 { if err != nil {
switch { switch {
case errors.Is(err, booking.ErrStripeClientNotConfigured): 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): 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): 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: 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 { func handleBookingUpdate(bs *booking.Service, hc *config.Host) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
type UpdateBooking struct { if err := r.ParseForm(); err != nil {
From time.Time `json:"from"` http.Error(w, err.Error(), http.StatusBadRequest)
To time.Time `json:"to"` return
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"`
} }
nb := new(UpdateBooking)
err := c.Bind(nb) id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil { if err != nil {
log.Warn(err) http.Error(w, "invalid booking id", http.StatusBadRequest)
return err return
} }
ts, _ := myTime.ParseFromForm(c.FormValue("from")) customerNumber, err := strconv.Atoi(r.FormValue("customer_number"))
nb.From = ts if err != nil {
ts, _ = myTime.ParseFromForm(c.FormValue("to")) http.Error(w, "invalid customer number", http.StatusBadRequest)
nb.To = ts return
if *nb.ExternalId == "" {
nb.ExternalId = nil
} }
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{ form := view.BookingForm(view.BookingViewModel{
Id: b.InvoiceNumber(hc), Id: b.InvoiceNumber(hc),
@ -261,7 +320,7 @@ func handleBookingUpdate(bs *booking.Service, hc *config.Host) echo.HandlerFunc
To: b.To.Format(time.DateOnly), To: b.To.Format(time.DateOnly),
Canceled: b.Canceled, Canceled: b.Canceled,
Platform: string(b.Platform), Platform: string(b.Platform),
ExternalId: *b.ExternalID, ExternalId: externalValue,
Platforms: u.Map(hc.Platforms, func(p config.Platform) string { return string(p) }), Platforms: u.Map(hc.Platforms, func(p config.Platform) string { return string(p) }),
PlatformFees: strconv.FormatFloat(b.PlatformFees, 'f', 2, 64), PlatformFees: strconv.FormatFloat(b.PlatformFees, 'f', 2, 64),
PaymentMethods: hc.PaymentMethods, 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)), 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 { func handleLineItemForm(bs *booking.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
idStr := c.Param("id") idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
return err http.Error(w, "invalid item id", http.StatusBadRequest)
return
} }
i := bs.OneItem(id) i := bs.OneItem(id)
form := view.LineItemForm(&view.ItemViewModel{ form := view.LineItemForm(&view.ItemViewModel{
Id: strconv.Itoa(i.ID), 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), SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.ID), 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 { func handleCreateItem(bs *booking.Service, hc *config.Host) http.HandlerFunc {
type NewItem struct { return func(w http.ResponseWriter, r *http.Request) {
Item string `form:"item"` bookingIdStr := chi.URLParam(r, "id")
PaymentMethod string `form:"method"`
Quantity int `form:"quantity"`
Price float64 `form:"price"`
}
return func(c echo.Context) error {
bookingIdStr := c.Param("id")
bid, err := strconv.Atoi(bookingIdStr) bid, err := strconv.Atoi(bookingIdStr)
if err != nil { 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) b := bs.One(bid)
ni := new(NewItem) itemName := r.FormValue("item")
if err := c.Bind(ni); err != nil { itm, ok := hc.Items[itemName]
log.Warn(err)
return err
}
itm, ok := hc.Items[ni.Item]
if !ok { 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 price := 0.0
// if err = cs.Create( if v := r.FormValue("price"); v != "" {
// itm.CalendarId, price, err = strconv.ParseFloat(v, 64)
// b.Name, if err != nil {
// fmt.Sprintf("Reservation: %s\n %d voyageur(s)\n", b.Name, b.CustomerNumber), http.Error(w, "invalid price", http.StatusBadRequest)
// b.From, b.To, return
// ); err != nil { }
// log.Warnf("could not create event: %s", err) }
// captureError(c, err)
// }
newItems := bs.CreateItem(b.ID, itm, quantity, price, r.FormValue("method"), b.CustomerNumber, string(b.Platform))
var buf bytes.Buffer
for _, i := range newItems { for _, i := range newItems {
_ = renderTempl(c, http.StatusCreated, view.LineItem(&view.ItemViewModel{ component := view.LineItem(&view.ItemViewModel{
Id: strconv.Itoa(i.ID), Id: strconv.Itoa(i.ID),
Item: i.Item, Item: i.Item,
Quantity: strconv.Itoa(i.Quantity), Quantity: strconv.Itoa(i.Quantity),
@ -344,23 +410,32 @@ func handleCreateItem(bs *booking.Service, hc *config.Host) echo.HandlerFunc {
PaymentStatus: i.PaymentStatus, PaymentStatus: i.PaymentStatus,
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64), SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.ID), 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 { func handleItemPay(bs *booking.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
itemIdStr := c.Param("id") itemIdStr := chi.URLParam(r, "id")
itemId, err := strconv.Atoi(itemIdStr) itemId, err := strconv.Atoi(itemIdStr)
if err != nil { if err != nil {
return err http.Error(w, "invalid item id", http.StatusBadRequest)
return
} }
i := bs.PayItem(itemId) 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, Id: itemIdStr,
Item: i.Item, Item: i.Item,
Quantity: strconv.Itoa(i.Quantity), Quantity: strconv.Itoa(i.Quantity),
@ -368,73 +443,104 @@ func handleItemPay(bs *booking.Service) echo.HandlerFunc {
PaymentStatus: i.PaymentStatus, PaymentStatus: i.PaymentStatus,
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64), SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.ID), 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 { func handleItemUpdate(bs *booking.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
type updateItem struct { if err := r.ParseForm(); err != nil {
Item string `form:"item"` http.Error(w, err.Error(), http.StatusBadRequest)
PaymentMethod string `form:"paymentMethod"` return
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
} }
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{ quantity, err := strconv.Atoi(r.FormValue("quantity"))
Id: strconv.Itoa(ui.Id), 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, Item: i.Item,
Quantity: strconv.Itoa(i.Quantity), Quantity: strconv.Itoa(i.Quantity),
Price: strconv.FormatFloat(i.Price, 'f', 2, 64), Price: strconv.FormatFloat(i.Price, 'f', 2, 64),
PaymentStatus: i.PaymentStatus, PaymentStatus: i.PaymentStatus,
SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64), SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.ID), 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 { func handlePaymentUpdate(bs *booking.Service) http.HandlerFunc {
type updatePayment struct { return func(w http.ResponseWriter, r *http.Request) {
Id int `param:"id"` if err := r.ParseForm(); err != nil {
Amount float64 `form:"amount"` http.Error(w, err.Error(), http.StatusBadRequest)
PaymentMethod string `form:"paymentMethod"` return
}
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)
} }
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 { func handleBookingCancel(bs *booking.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
idStr := c.Param("id") idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
return err http.Error(w, "invalid booking id", http.StatusBadRequest)
return
} }
bs.Cancel(id) bs.Cancel(id)
return renderTempl(c, http.StatusOK, templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { component := templ.ComponentFunc(func(ctx context.Context, writer io.Writer) error {
_, err := io.WriteString(w, " <span>Canceled</span>") _, err := io.WriteString(writer, " <span>Canceled</span>")
return err return err
})) })
if err := renderTempl(w, http.StatusOK, component); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} }
} }

View file

@ -2,12 +2,11 @@ package server
import ( import (
"net/http" "net/http"
"github.com/labstack/echo/v4"
) )
func handleHealthCheck() echo.HandlerFunc { func handleHealthCheck() http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, _ *http.Request) {
return c.String(http.StatusOK, "healthy") w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("healthy"))
} }
} }

View file

@ -4,58 +4,68 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/labstack/echo/v4" "github.com/go-chi/chi/v5"
u "github.com/rjNemo/underscore" u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/service/booking" "github.com/rjNemo/rentease/internal/service/booking"
"github.com/rjNemo/rentease/internal/view" "github.com/rjNemo/rentease/internal/view"
) )
func handleCreatePayment(bs *booking.Service) echo.HandlerFunc { func handleCreatePayment(bs *booking.Service) http.HandlerFunc {
type CreatePaymentInput struct { return func(w http.ResponseWriter, r *http.Request) {
Amount float64 `form:"amount"` if err := r.ParseForm(); err != nil {
PaymentMethod string `form:"paymentMethod"` http.Error(w, err.Error(), http.StatusBadRequest)
} return
return func(c echo.Context) error {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
return err
} }
np := new(CreatePaymentInput) id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err := c.Bind(np); err != nil { if err != nil {
return err 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) b := bs.One(id)
_, err = bs.CreatePayment(b.ID, np.Amount, np.PaymentMethod) if _, err := bs.CreatePayment(b.ID, amount, r.FormValue("paymentMethod")); err != nil {
if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError)
return err return
} }
nb := bs.One(id) nb := bs.One(id)
return renderTempl(c, http.StatusOK, view.PaymentList( component := view.PaymentList(
u.Map(nb.Payments, func(p booking.Payment) *view.PaymentViewModel { u.Map(nb.Payments, func(p booking.Payment) *view.PaymentViewModel {
return paymentViewModelFromBookingPayment(p) 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 { func handlePaymentForm(bs *booking.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
idStr := c.Param("id") id, err := strconv.Atoi(chi.URLParam(r, "id"))
id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
return err http.Error(w, "invalid payment id", http.StatusBadRequest)
return
} }
p := bs.OnePayment(id) p := bs.OnePayment(id)
form := view.PaymentForm(paymentViewModelFromBookingPayment(*p)) 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)
}
} }
} }

View file

@ -2,71 +2,68 @@ package server
import ( import (
"fmt" "fmt"
"log" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
"github.com/labstack/echo/v4" "github.com/go-chi/chi/v5"
u "github.com/rjNemo/underscore" u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/service/booking" "github.com/rjNemo/rentease/internal/service/booking"
) )
func handlePdfCreateInvoice(bs *booking.Service, hc *config.Host) echo.HandlerFunc { func handlePdfCreateInvoice(bs *booking.Service, hc *config.Host) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
idStr := c.Param("id") idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
log.Println(err) http.Error(w, "invalid booking id", http.StatusBadRequest)
return err return
} }
b := bs.One(id) b := bs.One(id)
filePath, err := bs.BuildInvoice(b, hc) filePath, err := bs.BuildInvoice(b, hc)
if err != nil { if err != nil {
log.Println(err) slog.Error("failed to build invoice", slog.Any("error", err))
return 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 { func handlePdfCreateReport(bs *booking.Service) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
period := c.QueryParam("period") period := r.URL.Query().Get("period")
if !u.Contains([]string{"month", "year"}, period) { if !u.Contains([]string{"month", "year"}, period) {
return &echo.HTTPError{ http.Error(w, fmt.Sprintf("%q is not a valid period", period), http.StatusBadRequest)
Code: http.StatusBadRequest, return
Message: fmt.Sprintf("%q is not a valid period", period),
}
} }
monthStr := c.QueryParam("month") monthStr := r.URL.Query().Get("month")
month, err := strconv.Atoi(monthStr) month, err := strconv.Atoi(monthStr)
if err != nil || month < 1 || month > 12 { if err != nil || month < 1 || month > 12 {
return &echo.HTTPError{ http.Error(w, fmt.Sprintf("%q is not a valid month", monthStr), http.StatusBadRequest)
Code: http.StatusBadRequest, return
Message: fmt.Sprintf("%q is not a valid month", month),
}
} }
yearStr := c.QueryParam("year") yearStr := r.URL.Query().Get("year")
year, err := strconv.Atoi(yearStr) year, err := strconv.Atoi(yearStr)
if err != nil { if err != nil {
return &echo.HTTPError{ http.Error(w, fmt.Sprintf("%q is not a valid year", yearStr), http.StatusBadRequest)
Code: http.StatusBadRequest, return
Message: fmt.Sprintf("%q is not a valid year", year),
}
} }
report := bs.Report(period, month, year) report := bs.Report(period, month, year)
filePath, err := bs.BuildReport(report, period, month, year) filePath, err := bs.BuildReport(report, period, month, year)
if err != nil { if err != nil {
return err http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
return c.File(filePath) http.ServeFile(w, r, filePath)
} }
} }

View file

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/labstack/echo/v4"
u "github.com/rjNemo/underscore" u "github.com/rjNemo/underscore"
"github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/config"
@ -16,64 +15,67 @@ import (
"github.com/rjNemo/rentease/internal/view" "github.com/rjNemo/rentease/internal/view"
) )
func handleReportsPage() echo.HandlerFunc { func handleReportsPage() http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
monthStr := c.QueryParam("month") monthStr := r.URL.Query().Get("month")
month, err := strconv.Atoi(monthStr) month, err := strconv.Atoi(monthStr)
if err != nil || month < 1 || month > 12 { if err != nil || month < 1 || month > 12 {
month = int(time.Now().Month()) month = int(time.Now().Month())
} }
yearStr := c.QueryParam("year") yearStr := r.URL.Query().Get("year")
_, err = strconv.Atoi(yearStr) if _, err = strconv.Atoi(yearStr); err != nil {
if err != nil {
yearStr = time.Now().Format("2006") 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) 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 { func handleReportCompute(bs *booking.Service, hc *config.Host) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
period := c.FormValue("period") if err := r.ParseForm(); err != nil {
if !u.Contains([]string{"month", "year"}, period) { http.Error(w, err.Error(), http.StatusBadRequest)
return &echo.HTTPError{ return
Code: http.StatusBadRequest,
Message: fmt.Sprintf("%q is not a valid period", period),
}
} }
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) month, err := strconv.Atoi(monthStr)
if err != nil || month < 1 || month > 12 { if err != nil || month < 1 || month > 12 {
return &echo.HTTPError{ http.Error(w, fmt.Sprintf("%q is not a valid month", monthStr), http.StatusBadRequest)
Code: http.StatusBadRequest, return
Message: fmt.Sprintf("%q is not a valid month", month),
}
} }
yearStr := c.FormValue("year") yearStr := r.FormValue("year")
year, err := strconv.Atoi(yearStr) year, err := strconv.Atoi(yearStr)
if err != nil { if err != nil {
return &echo.HTTPError{ http.Error(w, fmt.Sprintf("%q is not a valid year", yearStr), http.StatusBadRequest)
Code: http.StatusBadRequest, return
Message: fmt.Sprintf("%q is not a valid year", year),
}
} }
r := bs.Report(period, month, year) report := bs.Report(period, month, year)
reportVm := &view.ReportViewModel{ reportVm := &view.ReportViewModel{
Total: strconv.FormatFloat(r.Total, 'f', 2, 64), Total: strconv.FormatFloat(report.Total, 'f', 2, 64),
PlatformFees: strconv.FormatFloat(r.PlatformFees, 'f', 2, 64), PlatformFees: strconv.FormatFloat(report.PlatformFees, 'f', 2, 64),
Fee: strconv.FormatFloat(r.Fee, 'f', 2, 64), Fee: strconv.FormatFloat(report.Fee, 'f', 2, 64),
Profit: strconv.FormatFloat(r.Profit, 'f', 2, 64), Profit: strconv.FormatFloat(report.Profit, 'f', 2, 64),
CardTotal: strconv.FormatFloat(r.CardTotal, 'f', 2, 64), CardTotal: strconv.FormatFloat(report.CardTotal, 'f', 2, 64),
BookingFees: strconv.FormatFloat(r.BookingFees, '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)), 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{ return &view.ReportLine{
Id: l.InvoiceNumber(hc), Id: l.InvoiceNumber(hc),
Url: templ.SafeURL(fmt.Sprintf("%s/%d", constant.RouteBooking, l.ID)), 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)
}
} }
} }

View file

@ -8,8 +8,6 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/labstack/echo/v4"
"github.com/rjNemo/rentease/internal/service/booking" "github.com/rjNemo/rentease/internal/service/booking"
) )
@ -22,12 +20,13 @@ type stripeSyncer interface {
SyncStripePayments(ctx context.Context, from, to time.Time) error SyncStripePayments(ctx context.Context, from, to time.Time) error
} }
func handleStripeSync(bs stripeSyncer) echo.HandlerFunc { func handleStripeSync(bs stripeSyncer) http.HandlerFunc {
return func(c echo.Context) error { return func(w http.ResponseWriter, r *http.Request) {
req := new(stripeSyncRequest) 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) { 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 != "" { if req.From != "" {
parsed, err := time.Parse(time.RFC3339, req.From) parsed, err := time.Parse(time.RFC3339, req.From)
if err != nil { 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 from = parsed
} }
@ -46,18 +46,24 @@ func handleStripeSync(bs stripeSyncer) echo.HandlerFunc {
if req.To != "" { if req.To != "" {
parsed, err := time.Parse(time.RFC3339, req.To) parsed, err := time.Parse(time.RFC3339, req.To)
if err != nil { 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 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) { 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)
}
} }
} }

View file

@ -7,8 +7,6 @@ import (
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/labstack/echo/v4"
) )
type stubStripeSyncer struct { type stubStripeSyncer struct {
@ -31,15 +29,11 @@ func TestHandleStripeSyncSuccess(t *testing.T) {
from := now.Add(-2 * time.Hour).Format(time.RFC3339) from := now.Add(-2 * time.Hour).Format(time.RFC3339)
to := now.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 := 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() rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if err := handler(c); err != nil { handler(rec, req)
t.Fatalf("handler returned error: %v", err)
}
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code) t.Fatalf("expected status 200, got %d", rec.Code)
@ -54,22 +48,13 @@ func TestHandleStripeSyncInvalidTimestamp(t *testing.T) {
syncer := &stubStripeSyncer{} syncer := &stubStripeSyncer{}
handler := handleStripeSync(syncer) handler := handleStripeSync(syncer)
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/api/stripe/sync", strings.NewReader(`{"from":"not-a-date"}`)) 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() rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := handler(c) handler(rec, req)
if err == nil {
t.Fatal("expected error for invalid timestamp")
}
httpErr, ok := err.(*echo.HTTPError) if rec.Code != http.StatusBadRequest {
if !ok { t.Fatalf("expected status 400, got %d", rec.Code)
t.Fatalf("expected HTTPError, got %T", err)
}
if httpErr.Code != http.StatusBadRequest {
t.Fatalf("expected status 400, got %d", httpErr.Code)
} }
} }

View file

@ -6,7 +6,6 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/labstack/echo/v4"
"github.com/stripe/stripe-go/v83" "github.com/stripe/stripe-go/v83"
"github.com/stripe/stripe-go/v83/webhook" "github.com/stripe/stripe-go/v83/webhook"
) )
@ -16,50 +15,60 @@ type stripeEventService interface {
HandleChargeRefunded(ctx context.Context, ch *stripe.Charge) error HandleChargeRefunded(ctx context.Context, ch *stripe.Charge) error
} }
func handleStripeWebhook(bs stripeEventService, secret string) echo.HandlerFunc { var constructEvent = webhook.ConstructEvent
return func(c echo.Context) error {
func handleStripeWebhook(bs stripeEventService, secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if secret == "" { 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 { 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 == "" { 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 { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid webhook signature") http.Error(w, "invalid webhook signature", http.StatusBadRequest)
return
} }
switch event.Type { switch event.Type {
case stripe.EventTypePaymentIntentSucceeded: case stripe.EventTypePaymentIntentSucceeded:
var pi stripe.PaymentIntent var pi stripe.PaymentIntent
if err := json.Unmarshal(event.Data.Raw, &pi); err != nil { 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 { if err := bs.HandlePaymentIntentSucceeded(r.Context(), &pi); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
case stripe.EventTypeChargeRefunded: case stripe.EventTypeChargeRefunded:
var ch stripe.Charge var ch stripe.Charge
if err := json.Unmarshal(event.Data.Raw, &ch); err != nil { 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 { if err := bs.HandleChargeRefunded(r.Context(), &ch); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
default: default:
// Acknowledge events we don't actively process. // Acknowledge events we don't actively process.
} }
return c.NoContent(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
} }

View file

@ -2,18 +2,13 @@ package server
import ( import (
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"time"
"github.com/labstack/echo/v4"
"github.com/stripe/stripe-go/v83" "github.com/stripe/stripe-go/v83"
"github.com/stripe/stripe-go/v83/webhook"
) )
type stubStripeEventService struct { type stubStripeEventService struct {
@ -56,26 +51,18 @@ func TestHandleStripeWebhookPaymentIntent(t *testing.T) {
t.Fatalf("failed to marshal payload: %v", err) t.Fatalf("failed to marshal payload: %v", err)
} }
ts := time.Now() restore := stubConstructEvent(stripe.EventTypePaymentIntentSucceeded, payloadBytes)
sig := webhook.ComputeSignature(ts, payloadBytes, secret) defer restore()
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)
service := &stubStripeEventService{} service := &stubStripeEventService{}
handler := handleStripeWebhook(service, secret) handler := handleStripeWebhook(service, secret)
if err := handler(c); err != nil { req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader(string(payloadBytes)))
t.Fatalf("handler returned error: %v", err) req.Header.Set("Content-Type", "application/json")
} req.Header.Set("Stripe-Signature", "test")
rec := httptest.NewRecorder()
handler(rec, req)
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code) t.Fatalf("expected 200, got %d", rec.Code)
@ -105,26 +92,18 @@ func TestHandleStripeWebhookChargeRefunded(t *testing.T) {
} }
payloadBytes, _ := json.Marshal(payload) payloadBytes, _ := json.Marshal(payload)
ts := time.Now() restore := stubConstructEvent(stripe.EventTypeChargeRefunded, payloadBytes)
sig := webhook.ComputeSignature(ts, payloadBytes, secret) defer restore()
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)
service := &stubStripeEventService{} service := &stubStripeEventService{}
handler := handleStripeWebhook(service, secret) handler := handleStripeWebhook(service, secret)
if err := handler(c); err != nil { req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader(string(payloadBytes)))
t.Fatalf("handler returned error: %v", err) req.Header.Set("Content-Type", "application/json")
} req.Header.Set("Stripe-Signature", "test")
rec := httptest.NewRecorder()
handler(rec, req)
if !service.chargeCalled { if !service.chargeCalled {
t.Fatalf("expected charge handler to be called") t.Fatalf("expected charge handler to be called")
@ -132,19 +111,24 @@ func TestHandleStripeWebhookChargeRefunded(t *testing.T) {
} }
func TestHandleStripeWebhookInvalidSignature(t *testing.T) { func TestHandleStripeWebhookInvalidSignature(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader("{}")) req := httptest.NewRequest(http.MethodPost, "/webhooks/stripe", strings.NewReader("{}"))
req.Header.Set("Stripe-Signature", "invalid") req.Header.Set("Stripe-Signature", "invalid")
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := handleStripeWebhook(&stubStripeEventService{}, "secret") handler := handleStripeWebhook(&stubStripeEventService{}, "secret")
err := handler(c) handler(rec, req)
if err == nil {
t.Fatal("expected error for invalid signature")
}
if httpErr, ok := err.(*echo.HTTPError); !ok || httpErr.Code != http.StatusBadRequest { if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400 HTTP error, got %v", err) 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 }
}

View file

@ -3,16 +3,16 @@ package server
import ( import (
"context" "context"
"log" "log"
"net/http"
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/labstack/echo/v4"
) )
func renderTempl(c echo.Context, status int, t templ.Component) error { func renderTempl(w http.ResponseWriter, status int, t templ.Component) error {
c.Response().Writer.WriteHeader(status) w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
err := t.Render(context.Background(), c.Response().Writer) if err := t.Render(context.Background(), w); err != nil {
if err != nil {
log.Printf("failed to render response template %s", err) log.Printf("failed to render response template %s", err)
return err return err
} }
@ -20,29 +20,16 @@ func renderTempl(c echo.Context, status int, t templ.Component) error {
return nil return nil
} }
func hxRedirect(c echo.Context, statusCode int, url string) error { func hxRedirect(w http.ResponseWriter, statusCode int, url string) error {
c.Response().Header().Add("HX-Redirect", url) w.Header().Add("HX-Redirect", url)
return c.NoContent(statusCode) w.WriteHeader(statusCode)
return nil
} }
func hxRequest(c echo.Context) bool { func hxRequest(r *http.Request) bool {
header, ok := c.Request().Header["Hx-Request"] return r.Header.Get("Hx-Request") == "true"
if !ok {
return false
}
if header[0] != "true" {
return false
}
return true
} }
func hxBoosted(c echo.Context) bool { func hxBoosted(r *http.Request) bool {
header, ok := c.Request().Header["Hx-Boosted"] return r.Header.Get("Hx-Boosted") == "true"
if !ok {
return false
}
if header[0] != "true" {
return false
}
return true
} }

View file

@ -5,7 +5,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/labstack/echo/v4"
"github.com/rjNemo/underscore" "github.com/rjNemo/underscore"
) )
@ -15,31 +14,34 @@ const (
// CachingMiddleware adds caching headers. // CachingMiddleware adds caching headers.
// //
// ttl is the max age of the cache in seconds. If ttl is 0, the default value wil be used. // 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) echo.MiddlewareFunc { func CachingMiddleware(ttl int, fileTypes ...string) func(http.Handler) http.Handler {
if ttl == 0 { if ttl == 0 {
ttl = defaultTTL 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 { 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 { if shouldCache {
s := strings.Split(c.Request().RequestURI, "/") segments := strings.Split(r.URL.Path, "/")
etag := s[len(s)-1] etag := segments[len(segments)-1]
c.Response().Header().Set("Etag", etag) w.Header().Set("Etag", etag)
c.Response().Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", ttl)) w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", ttl))
if match := c.Request().Header.Get("If-None-Match"); match != "" {
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) { if strings.Contains(match, etag) {
return c.NoContent(http.StatusNotModified) w.WriteHeader(http.StatusNotModified)
return
} }
} }
} }
return next(c) next.ServeHTTP(w, r)
} })
} }
} }

View file

@ -1,50 +1,63 @@
package server package server
import ( import (
"github.com/labstack/echo/v4" "net/http"
"github.com/labstack/echo/v4/middleware"
"github.com/go-chi/chi/v5"
"github.com/rjNemo/rentease/internal/service/auth"
) )
func (s Server) MountHandlers() { func (s *Server) MountHandlers() {
s.Router.GET("/healthz", handleHealthCheck()) s.Router.Get("/healthz", handleHealthCheck())
s.Router.GET("/", handleLoginPage()) s.Router.Get("/", handleLoginPage())
s.Router.POST("/", handleLogin(s.as)) s.Router.Post("/", handleLogin(s.as))
s.Router.POST("/webhooks/stripe", handleStripeWebhook(s.bs, s.stripeWebhookSecret)) s.Router.Post("/webhooks/stripe", handleStripeWebhook(s.bs, s.stripeWebhookSecret))
api := s.Router.Group("/api") s.Router.Route("/api", func(r chi.Router) {
api.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ r.Use(apiKeyMiddleware(s.as))
KeyLookup: "header:api-key", r.Post("/sync", handleSync(s.bs))
Validator: func(key string, c echo.Context) (bool, error) { r.Get("/bookings", handleBookingList(s.bs))
return s.as.ValidateAPIKey(key), nil r.Post("/bookings", handleCreateBooking(s.bs))
}, r.Post("/stripe/sync", handleStripeSync(s.bs))
})) })
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))
private := s.Router.Group("") s.Router.Group(func(r chi.Router) {
private.Use(MakeAuthMiddleware(s.as)) r.Use(MakeAuthMiddleware(s.as))
private.GET("/bookings", handleBookingListPage(s.bs, s.hc)) r.Get("/bookings", handleBookingListPage(s.bs, s.hc))
private.GET("/bookings/new", handleBookingCreatePage(s.hc)) r.Get("/bookings/new", handleBookingCreatePage(s.hc))
private.POST("/bookings/new", handleBookingCreate(s.bs)) r.Post("/bookings/new", handleBookingCreate(s.bs))
private.GET("/bookings/:id", handleBookingPage(s.bs, s.hc)) r.Get("/bookings/{id}", handleBookingPage(s.bs, s.hc))
private.POST("/bookings/:id/stripe/payment-link", handleBookingStripePaymentLink(s.bs)) r.Post("/bookings/{id}/stripe/payment-link", handleBookingStripePaymentLink(s.bs))
private.PUT("/bookings/:id", handleBookingUpdate(s.bs, s.hc)) r.Put("/bookings/{id}", handleBookingUpdate(s.bs, s.hc))
private.PATCH("/bookings/:id/cancel", handleBookingCancel(s.bs)) r.Patch("/bookings/{id}/cancel", handleBookingCancel(s.bs))
private.POST("/bookings/:id/items", handleCreateItem(s.bs, s.hc)) r.Post("/bookings/{id}/items", handleCreateItem(s.bs, s.hc))
private.GET("/bookings/pdf/:id", handlePdfCreateInvoice(s.bs, s.hc)) r.Get("/bookings/pdf/{id}", handlePdfCreateInvoice(s.bs, s.hc))
private.POST("/items/:id", handleItemPay(s.bs)) r.Post("/items/{id}", handleItemPay(s.bs))
private.PUT("/items/:id", handleItemUpdate(s.bs)) r.Put("/items/{id}", handleItemUpdate(s.bs))
private.GET("/items/:id", handleLineItemForm(s.bs)) r.Get("/items/{id}", handleLineItemForm(s.bs))
private.GET("/reports", handleReportsPage()) r.Get("/reports", handleReportsPage())
private.GET("/reports/do", handleReportCompute(s.bs, s.hc)) r.Get("/reports/do", handleReportCompute(s.bs, s.hc))
private.GET("/reports/pdf", handlePdfCreateReport(s.bs)) r.Get("/reports/pdf", handlePdfCreateReport(s.bs))
private.POST("/payments/:id", handleCreatePayment(s.bs)) r.Post("/payments/{id}", handleCreatePayment(s.bs))
private.PUT("/payments/:id", handlePaymentUpdate(s.bs)) r.Put("/payments/{id}", handlePaymentUpdate(s.bs))
private.GET("/payments/:id", handlePaymentForm(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)
})
}
} }

View file

@ -5,17 +5,17 @@ import (
"embed" "embed"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"log/slog"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"time" "time"
"github.com/getsentry/sentry-go" sentryhttp "github.com/getsentry/sentry-go/http"
sentryecho "github.com/getsentry/sentry-go/echo" "github.com/go-chi/chi/v5"
"github.com/gorilla/sessions" "github.com/go-chi/chi/v5/middleware"
"github.com/labstack/echo-contrib/session" "github.com/go-chi/cors"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/service/auth" "github.com/rjNemo/rentease/internal/service/auth"
@ -23,7 +23,8 @@ import (
) )
type Server struct { type Server struct {
Router *echo.Echo Router *chi.Mux
httpServer *http.Server
bs *booking.Service bs *booking.Service
as *auth.Service as *auth.Service
hc *config.Host 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) { func New(bs *booking.Service, as *auth.Service, hc *config.Host, opts ...Option) (*Server, error) {
option := new(options) option := new(options)
for _, opt := range opts { for _, opt := range opts {
err := opt(option) if err := opt(option); err != nil {
if err != nil {
return nil, err return nil, err
} }
} }
router, err := NewRouter(*option.fs, *option.debug, option.origins)
if err != nil {
return nil, err
}
s := &Server{ s := &Server{
Router: NewRouter(*option.fs, *option.debug, *option.secretKey, option.origins), Router: router,
bs: bs, bs: bs,
as: as, as: as,
hc: hc, hc: hc,
@ -57,50 +62,73 @@ func New(bs *booking.Service, as *auth.Service, hc *config.Host, opts ...Option)
return s, nil 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() { go func() {
if err := s.Router.Start(s.addr); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.Router.Logger.Fatalf("shutting down the server: %s", err) slog.Error("shutting down the server", slog.Any("error", err))
os.Exit(1)
} }
}() }()
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt) signal.Notify(quit, os.Interrupt)
<-quit defer signal.Stop(quit)
ctx, cancel := context.WithTimeout(c, 10*time.Second)
select {
case <-quit:
case <-ctx.Done():
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() 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 { func NewRouter(filesystem embed.FS, debug bool, origins []string) (*chi.Mux, error) {
e := echo.New() r := chi.NewRouter()
// config _ = debug
e.HideBanner = !debug
e.Debug = debug r.Use(middleware.RequestID)
// middlewares r.Use(middleware.RealIP)
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ r.Use(middleware.Logger)
Format: "${time_rfc3339} [${method}: ${status}] ${uri}; ip=${remote_ip}; ${latency_human}; ${user_agent}\n", 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) { assetsFS, err := fs.Sub(filesystem, "assets")
if hub := sentryecho.GetHubFromContext(c); hub != nil { if err != nil {
hub.WithScope(func(s *sentry.Scope) { return nil, fmt.Errorf("failed to load static assets: %w", err)
hub.CaptureMessage(err.Error())
})
} }
fileServer := http.StripPrefix("/static/", http.FileServer(http.FS(assetsFS)))
r.Handle("/static/*", fileServer)
return r, nil
} }

View file

@ -2,34 +2,34 @@ package server
import ( import (
"fmt" "fmt"
"net/http"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/labstack/echo/v4"
) )
func SentryTracingMiddleware(next echo.HandlerFunc) echo.HandlerFunc { func SentryTracingMiddleware(next http.Handler) http.Handler {
return func(c echo.Context) error { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := c.Request().Context() ctx := r.Context()
hub := sentry.GetHubFromContext(ctx) hub := sentry.GetHubFromContext(ctx)
if hub == nil { if hub == nil {
// Check the concurrency guide for more details: https://docs.sentry.io/platforms/go/concurrency/
hub = sentry.CurrentHub().Clone() hub = sentry.CurrentHub().Clone()
ctx = sentry.SetHubOnContext(ctx, hub) ctx = sentry.SetHubOnContext(ctx, hub)
r = r.WithContext(ctx)
} }
options := []sentry.SpanOption{ options := []sentry.SpanOption{
// Set the OP based on values from https://develop.sentry.dev/sdk/performance/span-operations/
sentry.WithOpName("http.server"), sentry.WithOpName("http.server"),
sentry.ContinueFromRequest(c.Request()), sentry.ContinueFromRequest(r),
sentry.WithTransactionSource(sentry.SourceURL), sentry.WithTransactionSource(sentry.SourceURL),
} }
transaction := sentry.StartTransaction(ctx, transaction := sentry.StartTransaction(
fmt.Sprintf("%s %s", c.Request().Method, c.Request().URL.Path), ctx,
fmt.Sprintf("%s %s", r.Method, r.URL.Path),
options..., options...,
) )
defer transaction.Finish() defer transaction.Finish()
return next(c)
} next.ServeHTTP(w, r.WithContext(transaction.Context()))
})
} }

View file

@ -2,10 +2,9 @@ package auth
import ( import (
"errors" "errors"
"net/http"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/rjNemo/rentease/internal/constant" "github.com/rjNemo/rentease/internal/constant"
) )
@ -20,6 +19,7 @@ type Service struct {
admin string admin string
adminSecret string adminSecret string
apiKey string apiKey string
store *sessions.CookieStore
} }
func NewService(secret, admin, adminSecret, apiKey string) (*Service, error) { 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") 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{ return &Service{
secret, secret: secret,
admin, admin: admin,
adminSecret, adminSecret: adminSecret,
apiKey, apiKey: apiKey,
store: store,
}, nil }, nil
} }
@ -43,33 +51,27 @@ func (as *Service) ValidateAPIKey(key string) bool {
return key == as.apiKey return key == as.apiKey
} }
func (as *Service) getSession(c echo.Context) (*sessions.Session, error) { func (as *Service) getSession(r *http.Request) (*sessions.Session, error) {
sess, err := session.Get(sessionName, c) sess, err := as.store.Get(r, sessionName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
sess.Options = &sessions.Options{
Path: constant.RouteLogin,
MaxAge: sessionAge,
HttpOnly: true,
}
return sess, nil return sess, nil
} }
func (as *Service) Authenticate(c echo.Context, key string) error { func (as *Service) Authenticate(w http.ResponseWriter, r *http.Request, key string) error {
sess, err := as.getSession(c) sess, err := as.getSession(r)
if err != nil { if err != nil {
return err return err
} }
sess.Values["user"] = key sess.Values["user"] = key
return sess.Save(c.Request(), c.Response()) return sess.Save(r, w)
} }
func (as *Service) Authenticated(c echo.Context) bool { func (as *Service) Authenticated(r *http.Request) bool {
sess, err := as.getSession(c) sess, err := as.getSession(r)
if err != nil { if err != nil {
return false return false
} }