From 8d3f803474da5fd08d332f9533a4969167c7d5ed Mon Sep 17 00:00:00 2001 From: atridadl Date: Wed, 24 Jan 2024 11:22:33 -0700 Subject: [PATCH] Updated to BunRouter --- README.md | 6 +- api/ping.go | 8 +- api/sse.go | 24 +++--- api/ssedemosend.go | 30 +++++-- go.mod | 16 ++-- go.sum | 38 +++++---- lib/templates.go | 41 +++++++++ main.go | 56 ++++++------ middleware/ratelimit.go | 60 +++++++++++++ middleware/requestid.go | 41 +++++++++ middleware/secure.go | 21 +++++ pages/blog.go | 29 +++---- pages/home.go | 114 +++++++++++++++++++------ pages/post.go | 34 ++++---- pages/ssedemo.go | 25 ++---- pages/templates/partials/navitems.html | 2 +- pages/templates/ssedemo.html | 2 +- 17 files changed, 387 insertions(+), 160 deletions(-) create mode 100644 lib/templates.go create mode 100644 middleware/ratelimit.go create mode 100644 middleware/requestid.go create mode 100644 middleware/secure.go diff --git a/README.md b/README.md index 6cc4754..8546ec3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Go + Templates + HTMX ## Stack: -- Backend: Golang + Echo +- Backend: Golang + BunRouter - Rendering: Golang templates - Style: TailwindCSS + DaisyUI - Content format: Markdown @@ -19,5 +19,5 @@ Go + Templates + HTMX 5. Run ```air``` to start the dev server ## Tests -Without Coverage: `go test atri.dad/lib` -With Coverage: `go test atri.dad/lib -cover` \ No newline at end of file +Without Coverage: `go test goth.stack/lib` +With Coverage: `go test goth.stack/lib -cover` \ No newline at end of file diff --git a/api/ping.go b/api/ping.go index 1572391..19ace85 100644 --- a/api/ping.go +++ b/api/ping.go @@ -3,9 +3,11 @@ package api import ( "net/http" - "github.com/labstack/echo/v4" + "github.com/uptrace/bunrouter" ) -func Ping(c echo.Context) error { - return c.String(http.StatusOK, "Pong!") +func Ping(w http.ResponseWriter, req bunrouter.Request) error { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Pong!")) + return nil } diff --git a/api/sse.go b/api/sse.go index efc046b..1b30dfb 100644 --- a/api/sse.go +++ b/api/sse.go @@ -3,26 +3,28 @@ package api import ( "fmt" "log" + "net/http" "time" - "github.com/labstack/echo/v4" + "github.com/uptrace/bunrouter" "goth.stack/lib" ) -func SSEDemo(c echo.Context) error { - channel := c.QueryParam("channel") +func SSE(w http.ResponseWriter, req bunrouter.Request) error { + queryParams := req.URL.Query() + channel := queryParams.Get("channel") if channel == "" { channel = "default" } // Use the request context, which is cancelled when the client disconnects - ctx := c.Request().Context() + ctx := req.Context() pubsub, _ := lib.Subscribe(lib.RedisClient, channel) - c.Response().Header().Set(echo.HeaderContentType, "text/event-stream") - c.Response().Header().Set(echo.HeaderConnection, "keep-alive") - c.Response().Header().Set(echo.HeaderCacheControl, "no-cache") + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Cache-Control", "no-cache") // Create a ticker that fires every 15 seconds ticker := time.NewTicker(30 * time.Second) @@ -35,10 +37,10 @@ func SSEDemo(c echo.Context) error { return nil case <-ticker.C: // Every 30 seconds, send a comment to keep the connection alive - if _, err := c.Response().Write([]byte(": keep-alive\n\n")); err != nil { + if _, err := w.Write([]byte(": keep-alive\n\n")); err != nil { return err } - c.Response().Flush() + w.(http.Flusher).Flush() default: // Handle incoming messages as before msg, err := pubsub.ReceiveMessage(ctx) @@ -48,11 +50,11 @@ func SSEDemo(c echo.Context) error { } data := fmt.Sprintf("data: %s\n\n", msg.Payload) - if _, err := c.Response().Write([]byte(data)); err != nil { + if _, err := w.Write([]byte(data)); err != nil { return err } - c.Response().Flush() + w.(http.Flusher).Flush() } } } diff --git a/api/ssedemosend.go b/api/ssedemosend.go index 2416b74..997f155 100644 --- a/api/ssedemosend.go +++ b/api/ssedemosend.go @@ -1,25 +1,32 @@ package api import ( + "encoding/json" "net/http" - "github.com/labstack/echo/v4" + "github.com/uptrace/bunrouter" "goth.stack/lib" ) -func SSEDemoSend(c echo.Context) error { - channel := c.QueryParam("channel") +func SSEDemoSend(w http.ResponseWriter, req bunrouter.Request) error { + // Get query parameters + queryParams := req.URL.Query() + + // Get channel from query parameters + channel := queryParams.Get("channel") if channel == "" { channel = "default" } // Get message from query parameters, form value, or request body - message := c.QueryParam("message") + message := queryParams.Get("message") if message == "" { - message = c.FormValue("message") + message = req.PostFormValue("message") if message == "" { var body map[string]string - if err := c.Bind(&body); err != nil { + err := json.NewDecoder(req.Body).Decode(&body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return err } message = body["message"] @@ -27,11 +34,18 @@ func SSEDemoSend(c echo.Context) error { } if message == "" { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "message parameter is required"}) + errMsg := map[string]string{"error": "message parameter is required"} + errMsgBytes, _ := json.Marshal(errMsg) + http.Error(w, string(errMsgBytes), http.StatusBadRequest) + return nil } // Send message lib.SendSSE("default", message) - return c.JSON(http.StatusOK, map[string]string{"status": "message sent"}) + statusMsg := map[string]string{"status": "message sent"} + statusMsgBytes, _ := json.Marshal(statusMsg) + w.Write(statusMsgBytes) + + return nil } diff --git a/go.mod b/go.mod index 35ed6c2..8f51c80 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.21.6 require ( github.com/alecthomas/chroma/v2 v2.12.0 - github.com/labstack/echo/v4 v4.11.4 github.com/stretchr/testify v1.8.4 ) @@ -14,29 +13,30 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/alecthomas/assert/v2 v2.4.1 github.com/go-redis/redismock/v9 v9.2.0 - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 - github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/redis/go-redis/v9 v9.4.0 github.com/resendlabs/resend-go v1.7.0 - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/unrolled/secure v1.14.0 + github.com/uptrace/bunrouter v1.0.21 + github.com/uptrace/bunrouter/extra/reqlog v1.0.21 github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.5.0 // indirect gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 1258c12..db22b68 100644 --- a/go.sum +++ b/go.sum @@ -21,22 +21,22 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw= github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 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/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= -github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= -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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -58,17 +58,25 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE= +github.com/unrolled/secure v1.14.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/uptrace/bunrouter v1.0.21 h1:HXarvX+N834sXyHpl+I/TuE11m19kLW/qG5u3YpHUag= +github.com/uptrace/bunrouter v1.0.21/go.mod h1:TwT7Bc0ztF2Z2q/ZzMuSVkcb/Ig/d3MQeP2cxn3e1hI= +github.com/uptrace/bunrouter/extra/reqlog v1.0.21 h1:k9ebZATe9NOBUrPcMYJAQ2OMb62ERRN7qfwNwoa6Kxs= +github.com/uptrace/bunrouter/extra/reqlog v1.0.21/go.mod h1:j+pXrYzYe3OxTzFb4f/f2JL+CVoGVi1QJJiU0YKSUlw= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -77,8 +85,6 @@ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/lib/templates.go b/lib/templates.go new file mode 100644 index 0000000..677ae87 --- /dev/null +++ b/lib/templates.go @@ -0,0 +1,41 @@ +package lib + +import ( + "html/template" + "log" + "net/http" + "path/filepath" + "runtime" +) + +func RenderTemplate(w http.ResponseWriter, layout string, partials []string, props interface{}) error { + // Get the name of the current file + _, filename, _, _ := runtime.Caller(1) + page := filepath.Base(filename) + page = page[:len(page)-len(filepath.Ext(page))] // remove the file extension + + // Build the list of templates + templates := []string{ + "./pages/templates/layouts/" + layout + ".html", + "./pages/templates/" + page + ".html", + } + for _, partial := range partials { + templates = append(templates, "./pages/templates/partials/"+partial+".html") + } + + // Parse the templates + ts, err := template.ParseFiles(templates...) + if err != nil { + log.Print(err.Error()) + return err + } + + // Execute the layout template + err = ts.ExecuteTemplate(w, layout, props) + if err != nil { + log.Print(err.Error()) + return err + } + + return nil +} diff --git a/main.go b/main.go index c28ee7d..4451bdb 100644 --- a/main.go +++ b/main.go @@ -2,14 +2,17 @@ package main import ( "io" + "log" + "net/http" "text/template" "goth.stack/api" + "goth.stack/middleware" "goth.stack/pages" "github.com/joho/godotenv" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/uptrace/bunrouter" + "github.com/uptrace/bunrouter/extra/reqlog" ) // Template Type @@ -18,7 +21,7 @@ type Template struct { } // Template Render function -func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { +func (t *Template) Render(w io.Writer, name string, data interface{}) error { return t.templates.ExecuteTemplate(w, name, data) } @@ -26,38 +29,31 @@ func main() { godotenv.Load(".env") // Initialize router - e := echo.New() + router := bunrouter.New( + bunrouter.Use(reqlog.NewMiddleware(), middleware.RequestID, middleware.SecureHeaders), + ) - // Middlewares - e.Pre(middleware.RemoveTrailingSlash()) - e.Use(middleware.Logger()) - e.Use(middleware.RequestID()) - e.Use(middleware.Secure()) - e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ - Level: 5, - })) - e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(69))) + // Static server + fs := http.FileServer(http.Dir("public")) - // Template Parsing - t := &Template{ - templates: template.Must(template.ParseGlob("pages/**/*.html")), - } - - e.Renderer = t - - // // Static server - e.Static("/public", "public") + router.GET("/public/*filepath", func(w http.ResponseWriter, req bunrouter.Request) error { + http.StripPrefix("/public", fs).ServeHTTP(w, req.Request) + return nil + }) // Page routes - e.GET("/", pages.Home) - e.GET("/blog", pages.Blog) - e.GET("/post/:post", pages.Post) - e.GET("/ssedemo", pages.SSEDemo) + pageGroup := router.NewGroup("", bunrouter.Use(middleware.NewRateLimiter(50).RateLimit)) + pageGroup.GET("/", pages.Home) + pageGroup.GET("/blog", pages.Blog) + pageGroup.GET("/post/:post", pages.Post) + pageGroup.GET("/sse", pages.SSEDemo) // API Routes: - e.GET("/api/ping", api.Ping) - e.GET("/api/ssedemo", api.SSEDemo) - e.POST("/api/sendsse", api.SSEDemoSend) + apiGroup := router.NewGroup("/api") + apiGroup.GET("/ping", api.Ping) - e.Logger.Fatal(e.Start(":3000")) + apiGroup.GET("/sse", api.SSE) + apiGroup.POST("/sendsse", api.SSEDemoSend) + + log.Fatal(http.ListenAndServe(":3000", router)) } diff --git a/middleware/ratelimit.go b/middleware/ratelimit.go new file mode 100644 index 0000000..7e4a25d --- /dev/null +++ b/middleware/ratelimit.go @@ -0,0 +1,60 @@ +package middleware + +import ( + "net" + "net/http" + "sync" + "time" + + "github.com/uptrace/bunrouter" +) + +type rateLimiter struct { + visitors map[string]*visitor + mu sync.Mutex + rps int +} + +type visitor struct { + firstSeen time.Time + requests int +} + +func NewRateLimiter(rps int) *rateLimiter { + return &rateLimiter{ + visitors: make(map[string]*visitor), + rps: rps, + } +} + +func (rl *rateLimiter) RateLimit(next bunrouter.HandlerFunc) bunrouter.HandlerFunc { + return func(w http.ResponseWriter, req bunrouter.Request) error { + rl.mu.Lock() + defer rl.mu.Unlock() + + ip, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + // handle error, e.g., return an HTTP 500 error + http.Error(w, "Internal server error", http.StatusInternalServerError) + return nil + } + + v, exists := rl.visitors[ip] + if !exists || time.Since(v.firstSeen) > 1*time.Minute { + v = &visitor{ + firstSeen: time.Now(), + } + rl.visitors[ip] = v + } + + v.requests++ + + // Limit each IP to rps requests per minute + if v.requests > rl.rps { + http.Error(w, "Too many requests", http.StatusTooManyRequests) + return nil + } + + return next(w, req) + } +} diff --git a/middleware/requestid.go b/middleware/requestid.go new file mode 100644 index 0000000..30ce060 --- /dev/null +++ b/middleware/requestid.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "context" + "net/http" + + "github.com/google/uuid" + "github.com/uptrace/bunrouter" +) + +type contextKey string + +func (c contextKey) String() string { + return string(c) +} + +var ( + HeaderXRequestID = "X-Request-ID" + requestIDKey = contextKey("requestID") +) + +func RequestID(next bunrouter.HandlerFunc) bunrouter.HandlerFunc { + return func(w http.ResponseWriter, req bunrouter.Request) error { + reqID := req.Header.Get(HeaderXRequestID) + if reqID == "" { + reqID = uuid.New().String() + } + + ctx := context.WithValue(req.Context(), requestIDKey, reqID) + + w.Header().Set(HeaderXRequestID, reqID) + return next(w, req.WithContext(ctx)) + } +} + +func GetRequestID(ctx context.Context) string { + if reqID, ok := ctx.Value(requestIDKey).(string); ok { + return reqID + } + return "" +} diff --git a/middleware/secure.go b/middleware/secure.go new file mode 100644 index 0000000..fe97e05 --- /dev/null +++ b/middleware/secure.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "net/http" + + "github.com/unrolled/secure" + "github.com/uptrace/bunrouter" +) + +func SecureHeaders(next bunrouter.HandlerFunc) bunrouter.HandlerFunc { + secureMiddleware := secure.New(secure.Options{ + FrameDeny: true, + ContentTypeNosniff: true, + BrowserXssFilter: true, + }) + + return func(w http.ResponseWriter, req bunrouter.Request) error { + secureMiddleware.HandlerFuncWithNext(w, req.Request, nil) + return next(w, req) + } +} diff --git a/pages/blog.go b/pages/blog.go index ae633d3..2aaf354 100644 --- a/pages/blog.go +++ b/pages/blog.go @@ -1,7 +1,6 @@ package pages import ( - "html/template" "log" "net/http" "os" @@ -9,7 +8,7 @@ import ( "strings" "time" - "github.com/labstack/echo/v4" + "github.com/uptrace/bunrouter" "goth.stack/lib" ) @@ -17,18 +16,20 @@ type BlogProps struct { Posts []lib.CardLink } -func Blog(c echo.Context) error { +func Blog(w http.ResponseWriter, req bunrouter.Request) error { var posts []lib.CardLink files, err := os.ReadDir("./content/") if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "There was an finding posts!") + http.Error(w, "There was an issue finding posts!", http.StatusInternalServerError) + return nil } for _, file := range files { frontMatter, err := lib.ExtractFrontMatter(file, "./content/") if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering the posts!") + http.Error(w, "There was an issue rendering the posts!", http.StatusInternalServerError) + return nil } frontMatter.Href = "post/" + strings.TrimSuffix(file.Name(), ".md") @@ -57,19 +58,9 @@ func Blog(c echo.Context) error { Posts: posts, } - templates := []string{ - "./pages/templates/layouts/base.html", - "./pages/templates/partials/header.html", - "./pages/templates/partials/navitems.html", - "./pages/templates/partials/cardlinks.html", - "./pages/templates/blog.html", - } + // Specify the partials used by this page + partials := []string{"header", "navitems", "cardlinks"} - ts, err := template.ParseFiles(templates...) - if err != nil { - log.Print(err.Error()) - return err - } - - return ts.ExecuteTemplate(c.Response().Writer, "base", props) + // Render the template + return lib.RenderTemplate(w, "base", partials, props) } diff --git a/pages/home.go b/pages/home.go index d347745..57294a2 100644 --- a/pages/home.go +++ b/pages/home.go @@ -2,43 +2,115 @@ package pages import ( "html/template" - "log" + "net/http" - "github.com/labstack/echo/v4" + "github.com/uptrace/bunrouter" "goth.stack/lib" ) type HomeProps struct { - Socials []lib.IconLink - Tech []lib.IconLink - // Add more props here + Socials []lib.IconLink + Tech []lib.IconLink + ContractLink string + ResumeURL string + SupportLink string } -func Home(c echo.Context) error { +func Home(w http.ResponseWriter, req bunrouter.Request) error { socials := []lib.IconLink{ { Name: "Email", - Href: "mailto:me@atri.dad", + Href: "mailto:me@goth.stack", Icon: template.HTML(``), }, + { + Name: "Discord", + Href: "http://discord.com/users/83679718401904640", + Icon: template.HTML(``), + }, + { + Name: "Threads", + Href: "https://www.threads.net/@atridaddev", + Icon: template.HTML(``), + }, { Name: "GitHub", - Href: "https://github.com/atridadl/goth-stack", + Href: "https://github.com/atridadl", Icon: template.HTML(``), }, + { + Name: "LinkedIn", + Href: "https://www.linkedin.com/in/atridadl/", + Icon: template.HTML(``), + }, + { + Name: "Twitch", + Href: "https://www.twitch.tv/himbothyswaggins", + Icon: template.HTML(``), + }, + { + Name: "YouTube", + Href: "https://www.youtube.com/@himbothyswaggins", + Icon: template.HTML(``), + }, } tech := []lib.IconLink{ + { + Name: "React", + Href: "https://react.dev", + Icon: template.HTML(`React`), + }, + { + Name: "Remix", + Href: "https://remix.run", + Icon: template.HTML(`Remix`), + }, + { + Name: "TypeScript", + Href: "https://typescriptlang.org", + Icon: template.HTML(`TypeScript`), + }, + { + Name: "Node.js", + Href: "https://nodejs.org", + Icon: template.HTML(`Node.js`), + }, + { + Name: "Bun", + Href: "https://bun.sh", + Icon: template.HTML(`Bun`), + }, { Name: "Go", Href: "https://golang.org", Icon: template.HTML(`Go`), }, + { + Name: "C#", + Href: "https://docs.microsoft.com/en-us/dotnet/csharp/", + Icon: template.HTML(`C#`), + }, + { + Name: "PostgreSQL", + Href: "https://postgresql.org", + Icon: template.HTML(`PostgreSQL`), + }, + { + Name: "SQLite", + Href: "https://sqlite.org", + Icon: template.HTML(`SQLite`), + }, { Name: "Redis", Href: "https://redis.io", Icon: template.HTML(`Redis`), }, + { + Name: "OpenAI", + Href: "https://openai.com", + Icon: template.HTML(`OpenAI`), + }, { Name: "Fly.io", Href: "https://fly.io", @@ -52,23 +124,17 @@ func Home(c echo.Context) error { } props := HomeProps{ - Socials: socials, - Tech: tech, - // Add more props here + Socials: socials, + Tech: tech, + ContractLink: "mailto:contract@goth.stack", + ResumeURL: "https://srv.goth.stack/Atridad_Lahiji_Resume.pdf", + SupportLink: "https://donate.stripe.com/8wMeVF25c78L0V2288", } - templates := []string{ - "./pages/templates/layouts/base.html", - "./pages/templates/partials/header.html", - "./pages/templates/partials/navitems.html", - "./pages/templates/home.html", - } + // Specify the partials used by this page + partials := []string{"header", "navitems"} - ts, err := template.ParseFiles(templates...) - if err != nil { - log.Print(err.Error()) - return err - } - - return ts.ExecuteTemplate(c.Response().Writer, "base", props) + // Render the template + w.WriteHeader(http.StatusOK) + return lib.RenderTemplate(w, "base", partials, props) } diff --git a/pages/post.go b/pages/post.go index f4e8ffe..d252d39 100644 --- a/pages/post.go +++ b/pages/post.go @@ -7,7 +7,7 @@ import ( "os" chromahtml "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/labstack/echo/v4" + "github.com/uptrace/bunrouter" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting/v2" "gopkg.in/yaml.v2" @@ -21,24 +21,27 @@ type PostProps struct { Tags []string } -func Post(c echo.Context) error { - postName := c.ParamValues()[0] +func Post(w http.ResponseWriter, req bunrouter.Request) error { + postName := req.Param("post") filePath := "content/" + postName + ".md" md, err := os.ReadFile(filePath) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, "This post does not exist!") + http.Error(w, "This post does not exist!", http.StatusNotFound) + return nil } frontmatterBytes, content, err := lib.SplitFrontmatter(md) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!") + http.Error(w, "There was an issue rendering this post!", http.StatusInternalServerError) + return nil } var frontmatter lib.FrontMatter if err := yaml.Unmarshal(frontmatterBytes, &frontmatter); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!") + http.Error(w, "There was an issue rendering this post!", http.StatusInternalServerError) + return nil } var buf bytes.Buffer @@ -54,7 +57,8 @@ func Post(c echo.Context) error { ) if err := markdown.Convert(content, &buf); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!") + http.Error(w, "There was an issue rendering this post!", http.StatusInternalServerError) + return nil } props := PostProps{ @@ -64,17 +68,9 @@ func Post(c echo.Context) error { Tags: frontmatter.Tags, } - templates := []string{ - "./pages/templates/layouts/post.html", - "./pages/templates/partials/header.html", - "./pages/templates/partials/navitems.html", - "./pages/templates/post.html", - } + // Specify the partials used by this page + partials := []string{"header", "navitems"} - ts, err := template.ParseFiles(templates...) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!") - } - - return ts.ExecuteTemplate(c.Response().Writer, "post", props) + // Render the template + return lib.RenderTemplate(w, "post", partials, props) } diff --git a/pages/ssedemo.go b/pages/ssedemo.go index 6f58e8a..d3aa191 100644 --- a/pages/ssedemo.go +++ b/pages/ssedemo.go @@ -1,25 +1,16 @@ package pages import ( - "html/template" - "log" + "net/http" - "github.com/labstack/echo/v4" + "github.com/uptrace/bunrouter" + "goth.stack/lib" ) -func SSEDemo(c echo.Context) error { - templates := []string{ - "./pages/templates/layouts/base.html", - "./pages/templates/partials/header.html", - "./pages/templates/partials/navitems.html", - "./pages/templates/ssedemo.html", - } +func SSEDemo(w http.ResponseWriter, req bunrouter.Request) error { + // Specify the partials used by this page + partials := []string{"header", "navitems"} - ts, err := template.ParseFiles(templates...) - if err != nil { - log.Print(err.Error()) - return err - } - - return ts.ExecuteTemplate(c.Response().Writer, "base", nil) + // Render the template + return lib.RenderTemplate(w, "base", partials, nil) } diff --git a/pages/templates/partials/navitems.html b/pages/templates/partials/navitems.html index ef27d16..5f1d5be 100644 --- a/pages/templates/partials/navitems.html +++ b/pages/templates/partials/navitems.html @@ -5,7 +5,7 @@
  • - + SSE Demo
  • diff --git a/pages/templates/ssedemo.html b/pages/templates/ssedemo.html index 40c3aa3..31ccbe3 100644 --- a/pages/templates/ssedemo.html +++ b/pages/templates/ssedemo.html @@ -13,7 +13,7 @@ GOTH // SSE
    DEMO

    Server Sent Events

    This page demonstrates the use of the HTMX SSE Extention to receive Server Sent Events on the "default" channel.

    Any events received on the "default" channel will appear below:

    -
    +
    Waiting for SSE Message...