diff --git a/.air.toml b/.air.toml index 53197bd..a3dd87b 100644 --- a/.air.toml +++ b/.air.toml @@ -6,7 +6,7 @@ testdata_dir = "testdata" [build] args_bin = [] bin = "./tmp/server" - cmd = "CGO_ENABLED=1 go build -o ./tmp/server ./cmd/api/main.go" + cmd = "pkill server; CGO_ENABLED=1 go build -o ./tmp/server ./cmd/main.go" include_ext = ["go", "tpl", "tmpl", "html"] exclude_dir = ["assets", "tmp", "vendor", "testdata"] exclude_regex = ["(\\.null-ls.*|_test)\\.go"] diff --git a/Makefile b/Makefile index 7859db3..bd75c46 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ fmt: t: test test: ./tests/ - go test -v ./tests/ + go test -v ./... encrypt: .env gpg -c .env diff --git a/go.mod b/go.mod index 0267644..e7bf9c2 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,14 @@ module github.com/marco-souza/marco.fly.dev go 1.22 require ( + github.com/Shopify/go-lua v0.0.0-20240312125312-5d657e363856 github.com/go-playground/validator/v10 v10.16.0 github.com/gofiber/fiber/v2 v2.52.1 github.com/gofiber/storage/sqlite3/v2 v2.1.0 github.com/gofiber/template/html/v2 v2.0.5 github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.9.0 github.com/valyala/fasthttp v1.51.0 gorm.io/driver/sqlite v1.5.4 gorm.io/gorm v1.25.5 @@ -16,6 +18,7 @@ require ( require ( github.com/andybalholm/brotli v1.0.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -31,6 +34,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-sqlite3 v1.14.18 // indirect github.com/philhofer/fwd v1.1.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect @@ -39,4 +43,5 @@ require ( golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 80ac990..622fff5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Shopify/go-lua v0.0.0-20240312125312-5d657e363856 h1:N32EXb3LZ0FJwbdoMokLXysTHgWHccuYUPxmVqPowkw= +github.com/Shopify/go-lua v0.0.0-20240312125312-5d657e363856/go.mod h1:M4CxjVc/1Nwka5atBv7G/sb7Ac2BDe3+FxbiT9iVNIQ= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -59,8 +61,8 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -109,6 +111,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/entities/routes.go b/internal/entities/routes.go deleted file mode 100644 index 0869f7f..0000000 --- a/internal/entities/routes.go +++ /dev/null @@ -1,5 +0,0 @@ -package entities - -import "github.com/gofiber/fiber/v2" - -type Register func(router fiber.Router) diff --git a/internal/lua/lua.go b/internal/lua/lua.go new file mode 100644 index 0000000..d54017a --- /dev/null +++ b/internal/lua/lua.go @@ -0,0 +1,47 @@ +package lua + +import ( + "io" + "log" + "os" + + "github.com/Shopify/go-lua" +) + +type luaRuntime struct { + l *lua.State +} + +func new() *luaRuntime { + l := lua.NewState() + lua.OpenLibraries(l) + return &luaRuntime{l} +} + +func (r *luaRuntime) Run(snippet string) (string, error) { + log.Println("Running Lua snippet", snippet) + + outputReader, outputWriter, _ := os.Pipe() + rescueStdout := os.Stdout // save the actual stdout + os.Stdout = outputWriter // redirect stdout to pipe + + err := lua.DoString(r.l, snippet) + if err != nil { + log.Println("Error running Lua snippet", err) + return "", err + } + + outputWriter.Close() // close pipe writer + os.Stdout = rescueStdout // restore stdout + + output, err := io.ReadAll(outputReader) + if err != nil { + log.Println("Error reading Lua output", err) + return "", err + } + + log.Println("Lua output", string(output)) + return string(output), nil +} + +var Runtime = new() diff --git a/internal/lua/lua_test.go b/internal/lua/lua_test.go new file mode 100644 index 0000000..3833905 --- /dev/null +++ b/internal/lua/lua_test.go @@ -0,0 +1,27 @@ +package lua + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLua(t *testing.T) { + t.Run("should run code if valid", func(t *testing.T) { + snippet := "print(10^3)" + output, err := Runtime.Run(snippet) + assert.Nil(t, err) + assert.Contains(t, output, "1000") + }) + + t.Run("should error with invalid code", func(t *testing.T) { + _, err := Runtime.Run("(- (+ 1 1) 2)") + assert.Error(t, err, "syntax error") + + _, err = Runtime.Run("function js() { return 1+1 } js()") + assert.Error(t, err, "syntax error") + + _, err = Runtime.Run("invalid code") + assert.Error(t, err, "syntax error") + }) +} diff --git a/internal/server/routes/api/lua.go b/internal/server/routes/api/lua.go new file mode 100644 index 0000000..d40129d --- /dev/null +++ b/internal/server/routes/api/lua.go @@ -0,0 +1,30 @@ +package api + +import ( + "log" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/marco-souza/marco.fly.dev/internal/lua" +) + +func luaHandler(c *fiber.Ctx) error { + snippet := c.FormValue("snippet") + if snippet == "" { + log.Println("No snippet provided.") + lines := []string{"No output yet."} + return c.Render("partials/code", fiber.Map{"Lines": lines}, "layouts/empty") + } + + log.Println("Lua code:", snippet) + code, err := lua.Runtime.Run(snippet) + if err != nil { + log.Println("Lua error:", err) + lines := []string{err.Error()} + return c.Render("partials/code", fiber.Map{"Lines": lines}, "layouts/empty") + } + + log.Println("Lua output:", code) + lines := strings.Split(code, "\n") + return c.Render("partials/code", fiber.Map{"Lines": lines}, "layouts/empty") +} diff --git a/internal/server/routes/api/routes.go b/internal/server/routes/api/routes.go index f1445e2..159ca05 100644 --- a/internal/server/routes/api/routes.go +++ b/internal/server/routes/api/routes.go @@ -1,41 +1,63 @@ package api import ( + "log" + "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/storage/sqlite3/v2" "github.com/marco-souza/marco.fly.dev/internal/config" + "github.com/marco-souza/marco.fly.dev/internal/github" ) var conf = config.Load() +// create auth check middleware +func authMiddleware(c *fiber.Ctx) error { + token := github.AccessToken(c) + if token == "" { + log.Println("Unauthorized access, redirecting to login page.") + return c.Redirect(conf.Github.LoginPage) + } + return c.Next() +} + func Apply(router fiber.Router) { // https://docs.gofiber.io/api/middleware/limiter - router.Use(limiter.New(limiter.Config{ - Max: conf.RateLimit, - Storage: sqlite3.New(), - })) + if conf.Env == "production" { + router.Use(limiter.New(limiter.Config{ + Max: conf.RateLimit, + Storage: sqlite3.New(), + })) + } - router.Group("/orders"). - Get("/", ordersHandler). - Post("/", createOrderHandler). - Delete("/:id", deleteOrderHandler) + // public routes + router.Group("/"). + Get("/resume", resumeHandler). + Get("/menu", menuHandler) + // auth router.Group("/auth/github"). Get("/", redirectGithubAuth). Get("/callback", callbackGithubAuth). Get("/refresh", logoutGithubAuth). Get("/logout", logoutGithubAuth) - // ref into one Group - router.Group("/"). - Get("/now", nowHandler). - Get("/sse", sseHandler). - Get("/resume", resumeHandler). - Get("/menu", menuHandler) - if conf.Env == "development" { router.Get("/reload", sseReloadHandler) } + + // private routes + router.Use(authMiddleware) + + router.Group("/"). + Post("/lua", luaHandler). + Get("/now", nowHandler). + Get("/sse", sseHandler) + + router.Group("/orders"). + Get("/", ordersHandler). + Post("/", createOrderHandler). + Delete("/:id", deleteOrderHandler) } diff --git a/internal/server/server.go b/internal/server/server.go index 15a9faf..f2b3ffc 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,6 +3,8 @@ package server import ( "fmt" "log" + "os" + "os/signal" "time" "github.com/gofiber/fiber/v2" @@ -53,7 +55,20 @@ func (s *server) Start() { models.Seed() } - log.Fatal(s.app.Listen(s.addr)) + shutdown := make(chan os.Signal, 1) + signal.Notify(shutdown, os.Interrupt) // register channel to interrupt signals + teardown := func() { + <-shutdown + fmt.Println("shutting down server...") + s.app.Shutdown() + } + + go teardown() // start listening for interrupt signals + + // await for server to shutdown + if err := s.app.Listen(s.addr); err != nil { + log.Fatal(err) + } } func (s *server) setupRoutes() { diff --git a/todo.md b/todo.md index 2c5e117..9c98127 100644 --- a/todo.md +++ b/todo.md @@ -3,7 +3,6 @@ ## In-Progress // upcoming - [ ] handle errors for form validation - - [ ] make menu lazyloaded by htmx ## Backlog @@ -14,6 +13,7 @@ ## Done + - [x] make menu lazyloaded by htmx - [x] make resume page - [x] make resume lazy loadede by htmx - [x] add midleware for private routes diff --git a/views/partials/code.html b/views/partials/code.html new file mode 100644 index 0000000..f964452 --- /dev/null +++ b/views/partials/code.html @@ -0,0 +1,5 @@ + + {{range .Lines}} +

{{.}}

+ {{end}} +
diff --git a/views/partials/editor.html b/views/partials/editor.html new file mode 100644 index 0000000..4e930b9 --- /dev/null +++ b/views/partials/editor.html @@ -0,0 +1,19 @@ +
+
+ + + + + +
+ + No output yet. +
diff --git a/views/partials/footer.html b/views/partials/footer.html index eeaccce..239ec6d 100644 --- a/views/partials/footer.html +++ b/views/partials/footer.html @@ -1,3 +1,3 @@ diff --git a/views/partials/now.html b/views/partials/now.html index 6f63e21..65147b9 100644 --- a/views/partials/now.html +++ b/views/partials/now.html @@ -1,3 +1,3 @@ -

+

{{ .Time }}

diff --git a/views/playground.html b/views/playground.html index 622708e..5a578c8 100644 --- a/views/playground.html +++ b/views/playground.html @@ -2,7 +2,7 @@

{{.Title}}