package handler import ( "encoding/json" "fmt" "html/template" "io" "net/http" "os" "path/filepath" "strings" "time" "ridgwaysystems.org/website/internal/blog" "ridgwaysystems.org/website/internal/newsletter" "ridgwaysystems.org/website/internal/status" ) // AdminRouter dispatches /admin/* paths. func (h *Handler) AdminRouter(w http.ResponseWriter, r *http.Request) { path := r.URL.Path switch { case path == "/admin/login": if r.Method == http.MethodPost { h.adminLoginPost(w, r) } else { h.adminLoginGet(w, r) } case path == "/admin/logout": h.requireAuth(h.adminLogout)(w, r) case path == "/admin/new": if r.Method == http.MethodPost { h.requireAuth(h.adminNewPost)(w, r) } else { h.requireAuth(h.adminNewGet)(w, r) } case strings.HasPrefix(path, "/admin/edit/"): if r.Method == http.MethodPost { h.requireAuth(h.adminEditPost)(w, r) } else { h.requireAuth(h.adminEditGet)(w, r) } case strings.HasPrefix(path, "/admin/delete/"): h.requireAuth(h.adminDeletePost)(w, r) case path == "/admin/status": if r.Method == http.MethodPost { h.requireAuth(h.adminStatusPost)(w, r) } else { h.requireAuth(h.adminStatusGet)(w, r) } case path == "/admin/preview": h.requireAuth(h.adminPreview)(w, r) case path == "/admin/upload": h.requireAuth(h.adminUpload)(w, r) case path == "/admin/uploads": h.requireAuth(h.adminUploads)(w, r) case path == "/admin/newsletter": if r.Method == http.MethodPost { h.requireAuth(h.adminNewsletterDelete)(w, r) } else { h.requireAuth(h.adminNewsletter)(w, r) } default: h.renderErr(w, http.StatusNotFound, "Admin page not found.") } } // AdminDashboard handles GET /admin. func (h *Handler) AdminDashboard(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/admin" && r.URL.Path != "/admin/" { // Fall through to router h.AdminRouter(w, r) return } h.requireAuth(h.adminDashboard)(w, r) } type dashboardData struct { Posts []*blog.Post Flash string } func (h *Handler) adminDashboard(w http.ResponseWriter, r *http.Request) { flash := r.URL.Query().Get("flash") posts, err := h.store.All(true) if err != nil { h.renderErr(w, http.StatusInternalServerError, "Could not load posts.") return } h.render(w, "admin-dashboard", dashboardData{Posts: posts, Flash: flash}) } // --- Login --- func (h *Handler) adminLoginGet(w http.ResponseWriter, r *http.Request) { if isAuthenticated(r) { http.Redirect(w, r, "/admin", http.StatusSeeOther) return } h.render(w, "admin-login", map[string]string{"Error": ""}) } func (h *Handler) adminLoginPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { h.renderErr(w, http.StatusBadRequest, "Bad form data.") return } password := r.FormValue("password") if !checkPassword(password) { h.render(w, "admin-login", map[string]string{"Error": "Invalid password."}) return } setSession(w) http.Redirect(w, r, "/admin", http.StatusSeeOther) } func (h *Handler) adminLogout(w http.ResponseWriter, r *http.Request) { clearSession(w) http.Redirect(w, r, "/admin/login", http.StatusSeeOther) } // --- New post --- type editorData struct { Post *blog.Post Raw string IsNew bool Error string } func (h *Handler) adminNewGet(w http.ResponseWriter, r *http.Request) { now := time.Now().Format("2006-01-02") raw := fmt.Sprintf("---\ntitle: New Post\ndate: %s\ntags: []\ndraft: true\ndescription: \"\"\n---\n\n", now) h.render(w, "admin-editor", editorData{Raw: raw, IsNew: true}) } func (h *Handler) adminNewPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { h.renderErr(w, http.StatusBadRequest, "Bad form data.") return } slug := sanitizeSlug(r.FormValue("slug")) content := r.FormValue("content") if slug == "" { h.render(w, "admin-editor", editorData{Raw: content, IsNew: true, Error: "Slug is required."}) return } if err := h.store.Save(slug, content); err != nil { h.render(w, "admin-editor", editorData{Raw: content, IsNew: true, Error: "Failed to save: " + err.Error()}) return } http.Redirect(w, r, "/admin?flash=Post+created", http.StatusSeeOther) } // --- Edit post --- func (h *Handler) adminEditGet(w http.ResponseWriter, r *http.Request) { slug := strings.TrimPrefix(r.URL.Path, "/admin/edit/") raw, err := h.store.RawContent(slug) if err != nil { h.renderErr(w, http.StatusNotFound, "Post not found.") return } post, _ := h.store.Get(slug) h.render(w, "admin-editor", editorData{Post: post, Raw: raw, IsNew: false}) } func (h *Handler) adminEditPost(w http.ResponseWriter, r *http.Request) { slug := strings.TrimPrefix(r.URL.Path, "/admin/edit/") if err := r.ParseForm(); err != nil { h.renderErr(w, http.StatusBadRequest, "Bad form data.") return } content := r.FormValue("content") if err := h.store.Save(slug, content); err != nil { h.render(w, "admin-editor", editorData{Raw: content, Error: "Failed to save: " + err.Error()}) return } http.Redirect(w, r, "/admin?flash=Post+saved", http.StatusSeeOther) } // --- Delete post --- func (h *Handler) adminDeletePost(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { h.renderErr(w, http.StatusMethodNotAllowed, "POST required.") return } slug := strings.TrimPrefix(r.URL.Path, "/admin/delete/") if err := h.store.Delete(slug); err != nil { h.renderErr(w, http.StatusInternalServerError, "Delete failed: "+err.Error()) return } http.Redirect(w, r, "/admin?flash=Post+deleted", http.StatusSeeOther) } // --- Status editor --- type adminStatusData struct { JSON string Error string Flash string } func (h *Handler) adminStatusGet(w http.ResponseWriter, r *http.Request) { p, err := status.Load(filepath.Join(h.dataDir, "status.json")) if err != nil { h.render(w, "admin-status", adminStatusData{Error: "Could not load status.json: " + err.Error()}) return } raw, _ := json.MarshalIndent(p, "", " ") flash := r.URL.Query().Get("flash") h.render(w, "admin-status", adminStatusData{JSON: string(raw), Flash: flash}) } func (h *Handler) adminStatusPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { h.renderErr(w, http.StatusBadRequest, "Bad form data.") return } raw := r.FormValue("json") var p status.Page if err := json.Unmarshal([]byte(raw), &p); err != nil { h.render(w, "admin-status", adminStatusData{JSON: raw, Error: "Invalid JSON: " + err.Error()}) return } p.LastChecked = time.Now().UTC() if err := status.Save(filepath.Join(h.dataDir, "status.json"), &p); err != nil { h.render(w, "admin-status", adminStatusData{JSON: raw, Error: "Save failed: " + err.Error()}) return } http.Redirect(w, r, "/admin/status?flash=Saved", http.StatusSeeOther) } // --- Preview --- func (h *Handler) adminPreview(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { h.renderErr(w, http.StatusMethodNotAllowed, "POST required.") return } if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } content := r.FormValue("content") html, err := blog.RenderMarkdown(content) if err != nil { http.Error(w, "render error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, "