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) }