This updates gallery handling to support video playback with generated poster thumbnails, adds authenticated admin upload/delete flows, and improves dev/runtime behavior including reliable thumbnail generation and media-safe response handling. Co-authored-by: Cursor <cursoragent@cursor.com>
202 lines
5.1 KiB
Go
202 lines
5.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"technical.kiwi/website/internal/auth"
|
|
"technical.kiwi/website/internal/gallery"
|
|
"technical.kiwi/website/templates"
|
|
)
|
|
|
|
const maxUploadBytes = 32 << 20 // 32 MiB
|
|
|
|
func (s *Server) adminEnabled() bool {
|
|
return s.Auth.Enabled
|
|
}
|
|
|
|
func (s *Server) sessionToken(r *http.Request) string {
|
|
c, err := r.Cookie(auth.CookieName())
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return c.Value
|
|
}
|
|
|
|
func (s *Server) isAdmin(r *http.Request) bool {
|
|
return s.Sessions.Valid(s.sessionToken(r))
|
|
}
|
|
|
|
func (s *Server) setSession(w http.ResponseWriter, token string) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: auth.CookieName(),
|
|
Value: token,
|
|
Path: "/admin",
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: int((24 * time.Hour).Seconds()),
|
|
})
|
|
}
|
|
|
|
func (s *Server) clearSession(w http.ResponseWriter) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: auth.CookieName(),
|
|
Value: "",
|
|
Path: "/admin",
|
|
HttpOnly: true,
|
|
MaxAge: -1,
|
|
})
|
|
}
|
|
|
|
func (s *Server) requireAdmin(fn http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !s.adminEnabled() {
|
|
http.Error(w, "admin disabled", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
if !s.isAdmin(r) {
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
fn(w, r)
|
|
}
|
|
}
|
|
|
|
func (s *Server) reloadGallery() error {
|
|
s.galleryMu.Lock()
|
|
defer s.galleryMu.Unlock()
|
|
|
|
images, err := gallery.List(s.ImagesDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hero, hasHero := gallery.SelectHero(images)
|
|
s.Images = images
|
|
s.Hero = hero
|
|
s.HasHero = hasHero
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) adminLogin(w http.ResponseWriter, r *http.Request) {
|
|
if !s.adminEnabled() {
|
|
http.Error(w, "admin disabled: set ADMIN_USER and ADMIN_PASSWORD", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
if s.isAdmin(r) {
|
|
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
|
|
return
|
|
}
|
|
templates.AdminLogin("").Render(r.Context(), w)
|
|
}
|
|
|
|
func (s *Server) adminLoginPost(w http.ResponseWriter, r *http.Request) {
|
|
if !s.adminEnabled() {
|
|
http.Error(w, "admin disabled", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
templates.AdminLogin("Invalid request.").Render(r.Context(), w)
|
|
return
|
|
}
|
|
user := r.FormValue("username")
|
|
pass := r.FormValue("password")
|
|
if !auth.CheckCredentials(s.Auth, user, pass) {
|
|
templates.AdminLogin("Invalid username or password.").Render(r.Context(), w)
|
|
return
|
|
}
|
|
token, err := s.Sessions.Create()
|
|
if err != nil {
|
|
http.Error(w, "session error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
s.setSession(w, token)
|
|
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) adminLogout(w http.ResponseWriter, r *http.Request) {
|
|
s.Sessions.Delete(s.sessionToken(r))
|
|
s.clearSession(w)
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) adminIndex(w http.ResponseWriter, r *http.Request) {
|
|
images, _, _ := s.snapshot()
|
|
albums, _ := gallery.AlbumsOnDisk(s.ImagesDir)
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
templates.AdminDashboard(images, albums, "").Render(r.Context(), w)
|
|
}
|
|
|
|
func (s *Server) adminUpload(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxUploadBytes)
|
|
if err := r.ParseMultipartForm(maxUploadBytes); err != nil {
|
|
s.adminIndexError(w, r, "Upload too large or invalid.")
|
|
return
|
|
}
|
|
|
|
album := r.FormValue("album")
|
|
if album == "new" {
|
|
album = r.FormValue("album_new")
|
|
}
|
|
|
|
file, header, err := r.FormFile("image")
|
|
if err != nil {
|
|
s.adminIndexError(w, r, "Choose a JPEG image to upload.")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
rel, err := gallery.SaveUpload(s.ImagesDir, album, header.Filename, file)
|
|
if err != nil {
|
|
s.adminIndexError(w, r, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := gallery.EnsureDerivatives(s.ImagesDir, rel); err != nil {
|
|
log.Printf("admin upload derivatives: %v", err)
|
|
}
|
|
if err := s.reloadGallery(); err != nil {
|
|
log.Printf("admin reload gallery: %v", err)
|
|
}
|
|
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
images, _, _ := s.snapshot()
|
|
templates.AdminImageTable(images).Render(r.Context(), w)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) adminDelete(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
rel := r.FormValue("path")
|
|
if err := gallery.DeleteImage(s.ImagesDir, rel); err != nil {
|
|
s.adminIndexError(w, r, err.Error())
|
|
return
|
|
}
|
|
if err := s.reloadGallery(); err != nil {
|
|
log.Printf("admin reload gallery: %v", err)
|
|
}
|
|
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
images, _, _ := s.snapshot()
|
|
templates.AdminImageTable(images).Render(r.Context(), w)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) adminIndexError(w http.ResponseWriter, r *http.Request, msg string) {
|
|
images, _, _ := s.snapshot()
|
|
albums, _ := gallery.AlbumsOnDisk(s.ImagesDir)
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
templates.AdminFlash(msg).Render(r.Context(), w)
|
|
return
|
|
}
|
|
templates.AdminDashboard(images, albums, msg).Render(r.Context(), w)
|
|
}
|