Files
Artifacto/internal/server/admin.go
dtoro c7d7ee287c initial commit: Artifacto v0.1
Self-hosted HTML artifact publisher. Single Go binary, SQLite metadata,
HTML on disk. Features: password-protected artifacts, per-artifact expiration,
admin dashboard with view metrics (privacy-preserving daily-salt visitor hash,
sparklines, 30-day SVG chart, top referrers), rate-limited unlock endpoint.

Packaged as a Docker image (two-stage, CGO_ENABLED=0, pure-Go SQLite).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:45:57 +02:00

87 lines
2.2 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
}
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)
}