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