Files
Artifacto/internal/server/admin.go
claudio e74439b509 Auto-login from trusted reverse-proxy Authentik headers
When SSO_GATEWAY_SECRET is set and an incoming request carries both
X-Artifacto-Gateway (matching the secret) and X-Authentik-Username, the
admin middleware mints a session automatically so Authentik-authenticated
users skip the password form. Missing or wrong gateway header falls back
to the password-login flow, so peers that can reach the container
directly (bypassing the reverse proxy) cannot spoof Authentik identities.
2026-04-22 22:21:20 +02:00

97 lines
2.4 KiB
Go

package server
import (
"net/http"
"github.com/go-chi/chi/v5"
"git.hubris.network/dtoro/artifacto/internal/store"
)
type loginData struct {
Error string
Next string
}
func (s *Server) getLogin(w http.ResponseWriter, r *http.Request) {
// If already logged in, send to dashboard.
if c, err := r.Cookie("artifacto_admin"); err == nil && s.admin.Verify(c.Value) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// SSO: trusted gateway has proven identity — mint a session and skip the form.
if s.admin.HasValidSSO(r) {
s.admin.SetCookie(w)
next := r.URL.Query().Get("next")
if next == "" {
next = "/"
}
http.Redirect(w, r, next, http.StatusSeeOther)
return
}
s.render(w, "login", loginData{Next: r.URL.Query().Get("next")})
}
func (s *Server) postLogin(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "bad form", http.StatusBadRequest)
return
}
pw := r.FormValue("password")
if !s.admin.VerifyPassword(pw) {
s.render(w, "login", loginData{Error: "wrong password", Next: r.FormValue("next")})
return
}
s.admin.SetCookie(w)
next := r.FormValue("next")
if next == "" {
next = "/"
}
http.Redirect(w, r, next, http.StatusSeeOther)
}
func (s *Server) postLogout(w http.ResponseWriter, r *http.Request) {
s.admin.ClearCookie(w)
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
type dashboardData struct {
BaseURL string
Stats store.DashboardStats
Items []store.ListItem
FlashSlug string // newly-created slug, for copy-link affordance
}
func (s *Server) getDashboard(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListArtifacts()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stats, err := s.store.Dashboard()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s.render(w, "dashboard", dashboardData{
BaseURL: s.cfg.BaseURL,
Stats: stats,
Items: items,
FlashSlug: r.URL.Query().Get("new"),
})
}
func (s *Server) deleteArtifact(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug")
if err := s.store.DeleteArtifact(slug); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// HTMX removes the row on 200; non-HTMX redirects home.
if r.Header.Get("HX-Request") == "true" {
w.WriteHeader(http.StatusOK)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}