summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-04-25 21:25:40 -0600
committermo khan <mo@mokhan.ca>2025-04-28 09:07:31 -0600
commit9b01d1616e130a589151bf1273e41181ecc727f4 (patch)
tree639ec3b3c3857042a551c8e88b09413f590ebcec
parent13ab8de7d09b5d4b10132828277d17ba0543b901 (diff)
feat: use htmx to render partials
-rw-r--r--app/app.go2
-rw-r--r--app/controllers/dashboard/controller.go18
-rw-r--r--app/controllers/dashboard/controller_test.go4
-rw-r--r--app/controllers/dashboard/dto.go5
-rw-r--r--app/controllers/sessions/controller.go2
-rw-r--r--app/controllers/sparkles/controller.go18
-rw-r--r--app/controllers/sparkles/dto.go7
-rw-r--r--app/middleware/id_token.go26
-rw-r--r--app/middleware/id_token_test.go2
-rw-r--r--app/middleware/require_user.go2
-rw-r--r--app/middleware/require_user_test.go4
-rw-r--r--app/middleware/token_parser.go26
-rw-r--r--app/middleware/user.go2
-rw-r--r--app/views/dashboard/nav.html.tmpl22
-rw-r--r--app/views/dashboard/show.html.tmpl18
-rw-r--r--app/views/render.go6
-rw-r--r--app/views/sparkles/new.html.tmpl5
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--public/application.js1
-rw-r--r--public/index.html13
21 files changed, 126 insertions, 63 deletions
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 @@
+<nav>
+ <ul>
+ <li><strong>SparkleLab✨</strong></li>
+ </ul>
+ <ul>
+ {{ if .IsLoggedIn }}
+ <li>
+ <a href="{{ .CurrentUser.ProfileURL }}">
+ <img src="{{ .CurrentUser.Picture }}" />
+ {{ .CurrentUser.Username }}
+ </a>
+ </li>
+ <li>
+ <form action="/session/destroy" method="post">
+ <input type="submit" value="Logout">
+ </form>
+ </li>
+ {{ else }}
+ <li><a href="/session/new">Login</a></li>
+ {{ end }}
+ </ul>
+</nav>
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 @@
<meta name="color-scheme" content="light dark">
<title>SparkleLab</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="/application.js"></script>
</head>
<body>
- <header class="container">
- <nav>
- <ul>
- <li><strong>SparkleLab✨</strong></li>
- </ul>
- <ul>
- <li>
- <form action="/session/destroy" method="post">
- <input type="submit" value="Logout">
- </form>
- </li>
- </ul>
- </nav>
+ <header class="container" hx-get="/dashboard/nav" hx-trigger="load">
+ <progress />
</header>
<main id="app" class="container">
<form v-on:submit.prevent="submitSparkle">
- <label>/sparkle <input type="text" placeholder="@tanuki for helping me with my homework!" v-model="sparkle" /> </label>
+ <label>/sparkle <input type="text" placeholder="@tanuki for helping me with my homework!" v-model="sparkle" pattern="\s*(?<sparklee>@\w+)\s+(?<reason>.+)" required /> </label>
<button type="submit" v-bind:disabled="isDisabled">✨ Sparkle</button>
</form>
<span class="error">${ errorMessage }</span>
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 @@
+<form v-on:submit.prevent="submitSparkle">
+ <label>/sparkle <input type="text" placeholder="@tanuki for helping me with my homework!" v-model="sparkle" /> </label>
+ <button type="submit" v-bind:disabled="isDisabled">✨ Sparkle</button>
+</form>
+<span class="error">${ errorMessage }</span>
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 @@
<meta name="color-scheme" content="light dark">
<title>SparkleLab</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="/application.js"></script>
</head>
<body>
- <header class="container">
- <nav>
- <ul>
- <li><strong>SparkleLab✨</strong></li>
- </ul>
- <ul>
- <li><a href="/session/new">Login</a></li>
- </ul>
- </nav>
+ <header class="container" hx-get="/dashboard/nav" hx-trigger="load">
+ <progress />
</header>
<main id="app" class="container">
<h1>${ heading }</h1>
-
<article v-for="sparkle in recentSparkles">
<header>
<img :src="sparkle.author.picture" :alt="sparkle.author.username">