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(``), + }, + { + Name: "Remix", + Href: "https://remix.run", + Icon: template.HTML(``), + }, + { + Name: "TypeScript", + Href: "https://typescriptlang.org", + Icon: template.HTML(``), + }, + { + Name: "Node.js", + Href: "https://nodejs.org", + Icon: template.HTML(``), + }, + { + Name: "Bun", + Href: "https://bun.sh", + Icon: template.HTML(``), + }, { Name: "Go", Href: "https://golang.org", Icon: template.HTML(``), }, + { + Name: "C#", + Href: "https://docs.microsoft.com/en-us/dotnet/csharp/", + Icon: template.HTML(``), + }, + { + Name: "PostgreSQL", + Href: "https://postgresql.org", + Icon: template.HTML(``), + }, + { + Name: "SQLite", + Href: "https://sqlite.org", + Icon: template.HTML(``), + }, { Name: "Redis", Href: "https://redis.io", Icon: template.HTML(``), }, + { + Name: "OpenAI", + Href: "https://openai.com", + Icon: template.HTML(``), + }, { 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 @@
Any events received on the "default" channel will appear below:
-