From 9b01d1616e130a589151bf1273e41181ecc727f4 Mon Sep 17 00:00:00 2001 From: mo khan Date: Fri, 25 Apr 2025 21:25:40 -0600 Subject: feat: use htmx to render partials --- app/app.go | 2 +- app/controllers/dashboard/controller.go | 18 ++++++++++++++++++ app/controllers/dashboard/controller_test.go | 4 +++- app/controllers/dashboard/dto.go | 5 +++++ app/controllers/sessions/controller.go | 2 +- app/controllers/sparkles/controller.go | 18 ++++++++++++++++-- app/controllers/sparkles/dto.go | 7 +++++++ app/middleware/id_token.go | 26 ++++---------------------- app/middleware/id_token_test.go | 2 +- app/middleware/require_user.go | 2 +- app/middleware/require_user_test.go | 4 +++- app/middleware/token_parser.go | 26 ++++++++++++++++++++++++++ app/middleware/user.go | 2 +- app/views/dashboard/nav.html.tmpl | 22 ++++++++++++++++++++++ app/views/dashboard/show.html.tmpl | 18 ++++-------------- app/views/render.go | 6 +----- app/views/sparkles/new.html.tmpl | 5 +++++ go.mod | 2 +- go.sum | 4 ++-- public/application.js | 1 + public/index.html | 13 +++---------- 21 files changed, 126 insertions(+), 63 deletions(-) create mode 100644 app/controllers/sparkles/dto.go create mode 100644 app/middleware/token_parser.go create mode 100644 app/views/dashboard/nav.html.tmpl create mode 100644 app/views/sparkles/new.html.tmpl diff --git a/app/app.go b/app/app.go index 80ab9ce..d959c4d 100644 --- a/app/app.go +++ b/app/app.go @@ -40,6 +40,6 @@ func New(rootDir string) http.Handler { oidc := ioc.MustResolve[*oidc.OpenID](ioc.Default) users := ioc.MustResolve[domain.Repository[*domain.User]](ioc.Default) - chain := middleware.IDToken(oidc)(middleware.User(users)(mux)) + chain := middleware.IDToken(oidc, middleware.IDTokenFromSessionCookie)(middleware.User(users)(mux)) return log.HTTP(logger)(chain) } diff --git a/app/controllers/dashboard/controller.go b/app/controllers/dashboard/controller.go index 220871f..0f165ad 100644 --- a/app/controllers/dashboard/controller.go +++ b/app/controllers/dashboard/controller.go @@ -20,6 +20,7 @@ func (c *Controller) MountTo(mux *http.ServeMux) { requireUser := middleware.RequireUser() mux.Handle("GET /dashboard", requireUser(http.HandlerFunc(c.Show))) + mux.Handle("GET /dashboard/nav", http.HandlerFunc(c.Navigation)) } func (c *Controller) Show(w http.ResponseWriter, r *http.Request) { @@ -35,3 +36,20 @@ func (c *Controller) Show(w http.ResponseWriter, r *http.Request) { return } } + +func (c *Controller) Navigation(w http.ResponseWriter, r *http.Request) { + currentUser := cfg.CurrentUser.From(r.Context()) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "text/html") + + dto := &NavigationDTO{ + CurrentUser: currentUser, + IsLoggedIn: currentUser != nil, + } + if err := views.Render(w, "dashboard/nav", dto); err != nil { + log.WithFields(r.Context(), log.Fields{"error": err}) + w.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/app/controllers/dashboard/controller_test.go b/app/controllers/dashboard/controller_test.go index 629a03a..f6b2f43 100644 --- a/app/controllers/dashboard/controller_test.go +++ b/app/controllers/dashboard/controller_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/test" @@ -22,7 +23,8 @@ func TestController(t *testing.T) { mux.ServeHTTP(w, r) - assert.Equal(t, http.StatusNotFound, w.Code) + require.Equal(t, http.StatusFound, w.Code) + assert.Equal(t, "/", w.Header().Get("Location")) }) }) diff --git a/app/controllers/dashboard/dto.go b/app/controllers/dashboard/dto.go index 6ffe027..0a7f39d 100644 --- a/app/controllers/dashboard/dto.go +++ b/app/controllers/dashboard/dto.go @@ -5,3 +5,8 @@ import "gitlab.com/gitlab-org/software-supply-chain-security/authorization/spark type ViewDashboardDTO struct { CurrentUser *domain.User } + +type NavigationDTO struct { + IsLoggedIn bool + CurrentUser *domain.User +} diff --git a/app/controllers/sessions/controller.go b/app/controllers/sessions/controller.go index 9a65ae3..08002a2 100644 --- a/app/controllers/sessions/controller.go +++ b/app/controllers/sessions/controller.go @@ -136,5 +136,5 @@ func (c *Controller) Create(w http.ResponseWriter, r *http.Request) { } http.SetCookie(w, cookie.New("session", encoded, tokens.Expiry)) - http.Redirect(w, r, "/", http.StatusFound) + http.Redirect(w, r, "/dashboard", http.StatusFound) } diff --git a/app/controllers/sparkles/controller.go b/app/controllers/sparkles/controller.go index e0da8c4..35f2076 100644 --- a/app/controllers/sparkles/controller.go +++ b/app/controllers/sparkles/controller.go @@ -22,14 +22,28 @@ func New(db domain.Repository[*domain.Sparkle]) *Controller { } func (c *Controller) MountTo(mux *http.ServeMux) { - requireUser := middleware.RequireUser(http.StatusFound, "/") + requireUser := middleware.RequireUser() mux.HandleFunc("GET /sparkles", c.Index) + mux.Handle("GET /sparkles/new", requireUser(http.HandlerFunc(c.NewForm))) mux.Handle("POST /sparkles", requireUser(http.HandlerFunc(c.Create))) } func (c *Controller) Index(w http.ResponseWriter, r *http.Request) { - serde.ToHTTP(w, r, c.db.All()) + if err := serde.ToHTTP(w, r, c.db.All()); err != nil { + log.WithFields(r.Context(), log.Fields{"error": err}) + w.WriteHeader(http.StatusInternalServerError) + } +} + +func (c *Controller) NewForm(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "text/html") + + dto := &NewSparkleDTO{CurrentUser: cfg.CurrentUser.From(r.Context())} + if err := views.Render(w, "sparkles/new", dto); err != nil { + log.WithFields(r.Context(), log.Fields{"error": err}) + } } func (c *Controller) Create(w http.ResponseWriter, r *http.Request) { diff --git a/app/controllers/sparkles/dto.go b/app/controllers/sparkles/dto.go new file mode 100644 index 0000000..5e53dab --- /dev/null +++ b/app/controllers/sparkles/dto.go @@ -0,0 +1,7 @@ +package sparkles + +import "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" + +type NewSparkleDTO struct { + CurrentUser *domain.User +} diff --git a/app/middleware/id_token.go b/app/middleware/id_token.go index da39f43..f0a3c74 100644 --- a/app/middleware/id_token.go +++ b/app/middleware/id_token.go @@ -7,38 +7,20 @@ import ( "github.com/xlgmokha/x/pkg/x" xcfg "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/web/cookie" ) -type TokenParser func(*http.Request) oidc.RawToken - -func IDTokenFromSessionCookie(r *http.Request) oidc.RawToken { - cookies := r.CookiesNamed("session") - - if len(cookies) != 1 { - return "" - } - - tokens, err := oidc.TokensFromBase64String(cookies[0].Value) - if err != nil { - log.WithFields(r.Context(), log.Fields{"error": err}) - return "" - } - - return tokens.IDToken -} - -func IDToken(cfg *oidc.OpenID) func(http.Handler) http.Handler { - parsers := []TokenParser{IDTokenFromSessionCookie} - +func IDToken(cfg *oidc.OpenID, parsers ...TokenParser) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for _, parser := range parsers { rawIDToken := parser(r) - if !x.IsZero(rawIDToken) { + if x.IsPresent(rawIDToken) { verifier := cfg.Provider.VerifierContext(r.Context(), cfg.OIDCConfig) idToken, err := verifier.Verify(r.Context(), rawIDToken.String()) if err != nil { log.WithFields(r.Context(), log.Fields{"error": err}) + cookie.Expire(w, r, "session") } else { log.WithFields(r.Context(), log.Fields{"id_token": idToken}) next.ServeHTTP( diff --git a/app/middleware/id_token_test.go b/app/middleware/id_token_test.go index 607c028..53ac126 100644 --- a/app/middleware/id_token_test.go +++ b/app/middleware/id_token_test.go @@ -36,7 +36,7 @@ func TestIDToken(t *testing.T) { ) require.NoError(t, err) - middleware := IDToken(openID) + middleware := IDToken(openID, IDTokenFromSessionCookie) t.Run("when an active session cookie is provided", func(t *testing.T) { t.Run("attaches the token to the request context", func(t *testing.T) { diff --git a/app/middleware/require_user.go b/app/middleware/require_user.go index d0d5355..8f54a04 100644 --- a/app/middleware/require_user.go +++ b/app/middleware/require_user.go @@ -10,7 +10,7 @@ func RequireUser() func(http.Handler) http.Handler { if IsLoggedIn(r) { next.ServeHTTP(w, r) } else { - w.WriteHeader(http.StatusNotFound) + http.Redirect(w, r, "/", http.StatusFound) } }) } diff --git a/app/middleware/require_user_test.go b/app/middleware/require_user_test.go index 48afff7..794f347 100644 --- a/app/middleware/require_user_test.go +++ b/app/middleware/require_user_test.go @@ -4,6 +4,7 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" @@ -22,7 +23,8 @@ func TestRequireUser(t *testing.T) { })) server.ServeHTTP(w, r) - require.Equal(t, http.StatusNotFound, w.Code) + require.Equal(t, http.StatusFound, w.Code) + assert.Equal(t, "/", w.Header().Get("Location")) }) }) diff --git a/app/middleware/token_parser.go b/app/middleware/token_parser.go new file mode 100644 index 0000000..a719b2f --- /dev/null +++ b/app/middleware/token_parser.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "net/http" + + "github.com/xlgmokha/x/pkg/log" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc" +) + +type TokenParser func(*http.Request) oidc.RawToken + +func IDTokenFromSessionCookie(r *http.Request) oidc.RawToken { + cookies := r.CookiesNamed("session") + + if len(cookies) != 1 { + return "" + } + + tokens, err := oidc.TokensFromBase64String(cookies[0].Value) + if err != nil { + log.WithFields(r.Context(), log.Fields{"error": err}) + return "" + } + + return tokens.IDToken +} diff --git a/app/middleware/user.go b/app/middleware/user.go index e2f1ce3..21455ba 100644 --- a/app/middleware/user.go +++ b/app/middleware/user.go @@ -21,7 +21,7 @@ func User(db domain.Repository[*domain.User]) func(http.Handler) http.Handler { } user := db.Find(domain.ID(idToken.Subject)) - if x.IsZero(user) { + if !x.IsPresent(user) { user = mapper.MapFrom[*oidc.IDToken, *domain.User](idToken) if err := db.Save(user); err != nil { log.WithFields(r.Context(), log.Fields{"error": err}) diff --git a/app/views/dashboard/nav.html.tmpl b/app/views/dashboard/nav.html.tmpl new file mode 100644 index 0000000..59ff68b --- /dev/null +++ b/app/views/dashboard/nav.html.tmpl @@ -0,0 +1,22 @@ + diff --git a/app/views/dashboard/show.html.tmpl b/app/views/dashboard/show.html.tmpl index 664777e..3a29dd3 100644 --- a/app/views/dashboard/show.html.tmpl +++ b/app/views/dashboard/show.html.tmpl @@ -6,28 +6,18 @@ SparkleLab + -
- +
+
- +
${ errorMessage } diff --git a/app/views/render.go b/app/views/render.go index d06a4f7..a852e10 100644 --- a/app/views/render.go +++ b/app/views/render.go @@ -19,9 +19,5 @@ func Render[T any](w io.Writer, path string, data T) error { return err } - if err := tmpl.Execute(w, data); err != nil { - return err - } - - return nil + return tmpl.Execute(w, data) } diff --git a/app/views/sparkles/new.html.tmpl b/app/views/sparkles/new.html.tmpl new file mode 100644 index 0000000..077cb54 --- /dev/null +++ b/app/views/sparkles/new.html.tmpl @@ -0,0 +1,5 @@ +
+ + +
+${ errorMessage } diff --git a/go.mod b/go.mod index cf4dbba..13eb423 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.36.0 - github.com/xlgmokha/x v0.0.0-20250424234727-a05c79569767 + github.com/xlgmokha/x v0.0.0-20250425224307-3bca1eacf7f5 golang.org/x/oauth2 v0.29.0 ) diff --git a/go.sum b/go.sum index 11b5706..7a1aafd 100644 --- a/go.sum +++ b/go.sum @@ -132,8 +132,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/xlgmokha/x v0.0.0-20250424234727-a05c79569767 h1:DhDLgiHyEzhMPWW9bNgWP25+0v6+eUKskbwu9W2zOME= -github.com/xlgmokha/x v0.0.0-20250424234727-a05c79569767/go.mod h1:axGPKzoJCNTmPJxYqN5l+Z9gGbPe0yolkT61a5p3QiI= +github.com/xlgmokha/x v0.0.0-20250425224307-3bca1eacf7f5 h1:WTxSCjaoo9xIabUBOPKCl8aXAVfQ93Cqj1QZSK08/wk= +github.com/xlgmokha/x v0.0.0-20250425224307-3bca1eacf7f5/go.mod h1:axGPKzoJCNTmPJxYqN5l+Z9gGbPe0yolkT61a5p3QiI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/public/application.js b/public/application.js index 35ea2b6..ba478a8 100644 --- a/public/application.js +++ b/public/application.js @@ -73,5 +73,6 @@ document.addEventListener('DOMContentLoaded', (event) => { app.config.compilerOptions.delimiters = ["${", "}"]; app.mount('#app') + htmx.logAll(); }) diff --git a/public/index.html b/public/index.html index 988d729..c918c7e 100644 --- a/public/index.html +++ b/public/index.html @@ -6,24 +6,17 @@ SparkleLab + -
- +
+

${ heading }

-
-- cgit v1.2.3