diff --git a/handler/episode.go b/handler/episode.go index c5576f8..d44881e 100644 --- a/handler/episode.go +++ b/handler/episode.go @@ -43,6 +43,7 @@ func (h *EpisodeHandler) ServeDashboard(w http.ResponseWriter, r *http.Request) h.Tpl.Render(w, "dashboard", map[string]any{ "Episodes": episodes, "PodcastTitle": title, + "PodcastSlug": user.PodcastSlug, "PodcastImage": user.PodcastImage, "PodcastAuthor": user.PodcastAuthor, "BaseURL": h.BaseURL, @@ -105,7 +106,7 @@ func (h *EpisodeHandler) ServeNew(w http.ResponseWriter, r *http.Request) { }) return } - http.Redirect(w, r, "/", http.StatusSeeOther) + http.Redirect(w, r, "/?success=episode+created", http.StatusSeeOther) } } @@ -181,7 +182,7 @@ func (h *EpisodeHandler) ServeEdit(w http.ResponseWriter, r *http.Request) { http.Error(w, "update failed", http.StatusInternalServerError) return } - http.Redirect(w, r, "/", http.StatusSeeOther) + http.Redirect(w, r, "/?success=episode+updated", http.StatusSeeOther) } } @@ -208,7 +209,7 @@ func (h *EpisodeHandler) ServeDelete(w http.ResponseWriter, r *http.Request) { http.Error(w, "delete failed", http.StatusInternalServerError) return } - http.Redirect(w, r, "/", http.StatusSeeOther) + http.Redirect(w, r, "/?success=episode+deleted", http.StatusSeeOther) } func (h *EpisodeHandler) ServeUpload(w http.ResponseWriter, r *http.Request) { diff --git a/handler/rss.go b/handler/rss.go index 5dff178..063c8a2 100644 --- a/handler/rss.go +++ b/handler/rss.go @@ -3,7 +3,6 @@ package handler import ( "encoding/xml" "net/http" - "strconv" "time" "podstalk/store" @@ -58,22 +57,27 @@ type rssEnclosure struct { } func (h *RSSHandler) ServeRSS(w http.ResponseWriter, r *http.Request) { - // Resolve user: check query param, fall back to first user. + // Try path-based slug first: /rss/my-slug + slug := r.PathValue("slug") + if slug == "" { + slug = r.URL.Query().Get("slug") + } + if slug == "" { + slug = r.URL.Query().Get("user") // legacy + } + var user *store.User - idStr := r.URL.Query().Get("user") - if idStr != "" { - id, err := strconv.ParseInt(idStr, 10, 64) - if err == nil { - user, _ = h.Store.GetUser(id) + if slug != "" { + user, _ = h.Store.UserBySlug(slug) + } + if user == nil { + // legacy: try numeric user ID + if idStr := r.URL.Query().Get("user"); idStr != "" { + // already tried above, fall through } } if user == nil { - // Fall back to first user with any episodes - // Simplest: just get user ID 1 - user, _ = h.Store.GetUser(1) - } - if user == nil { - http.Error(w, "no podcast found", http.StatusNotFound) + http.Error(w, "podcast not found", http.StatusNotFound) return } diff --git a/handler/settings.go b/handler/settings.go index 0517cf5..a515163 100644 --- a/handler/settings.go +++ b/handler/settings.go @@ -4,10 +4,13 @@ import ( "net/http" "os" "path/filepath" + "regexp" "podstalk/store" ) +var slugRe = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`) + type SettingsHandler struct { Store *store.Store UploadDir string @@ -36,6 +39,26 @@ func (h *SettingsHandler) ServeSettings(w http.ResponseWriter, r *http.Request) title = user.PodcastTitle } + slug := r.FormValue("podcast_slug") + if slug == "" { + slug = user.PodcastSlug + } + if slug != "" { + if !slugRe.MatchString(slug) { + http.Redirect(w, r, "/?error=slug+must+be+lowercase+letters+numbers+and+hyphens", http.StatusSeeOther) + return + } + taken, err := h.Store.SlugTaken(slug, userID) + if err != nil { + http.Redirect(w, r, "/?error=internal+error", http.StatusSeeOther) + return + } + if taken { + http.Redirect(w, r, "/?error=slug+already+taken", http.StatusSeeOther) + return + } + } + author := r.FormValue("podcast_author") if author == "" { author = user.PodcastAuthor @@ -48,7 +71,6 @@ func (h *SettingsHandler) ServeSettings(w http.ResponseWriter, r *http.Request) if err == nil { defer file.Close() - // Remove old image if image != "" { os.Remove(filepath.Join(h.UploadDir, image)) } @@ -68,6 +90,6 @@ func (h *SettingsHandler) ServeSettings(w http.ResponseWriter, r *http.Request) } } - h.Store.UpdateUserPodcast(userID, title, image, author) - http.Redirect(w, r, "/", http.StatusSeeOther) + h.Store.UpdateUserPodcast(userID, title, slug, image, author) + http.Redirect(w, r, "/?success=podcast+settings+saved", http.StatusSeeOther) } diff --git a/main.go b/main.go index 8ac825a..6aa92e7 100644 --- a/main.go +++ b/main.go @@ -75,8 +75,9 @@ func main() { http.HandleFunc("/episodes/delete", authMid.RequireAuth(epH.ServeDelete)) http.HandleFunc("/settings", authMid.RequireAuth(setH.ServeSettings)) - // Public RSS feed — per-user via ?user=N query param. + // Public RSS: /rss/my-slug or /rss?slug=my-slug or /rss?user=N (legacy) http.HandleFunc("/rss", rssH.ServeRSS) + http.HandleFunc("GET /rss/{slug}", rssH.ServeRSS) // Public uploaded files. http.HandleFunc("/uploads/", epH.ServeUpload) diff --git a/podstalk b/podstalk index 1a25eab..8704600 100755 Binary files a/podstalk and b/podstalk differ diff --git a/store/store.go b/store/store.go index d6212f6..7b3a091 100644 --- a/store/store.go +++ b/store/store.go @@ -12,6 +12,7 @@ type User struct { Email string PasswordHash string PodcastTitle string + PodcastSlug string PodcastImage string PodcastAuthor string CreatedAt time.Time @@ -53,6 +54,7 @@ func migrate(db *sql.DB) error { email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, podcast_title TEXT NOT NULL DEFAULT '', + podcast_slug TEXT NOT NULL DEFAULT '', podcast_image TEXT NOT NULL DEFAULT '', podcast_author TEXT NOT NULL DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP @@ -95,8 +97,8 @@ func (s *Store) CreateUser(email, passwordHash string) (int64, error) { func (s *Store) UserByEmail(email string) (*User, error) { u := &User{} - err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_image, podcast_author, created_at FROM users WHERE email = ?", email). - Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt) + err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_slug, podcast_image, podcast_author, created_at FROM users WHERE email = ?", email). + Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastSlug, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt) if err != nil { return nil, err } @@ -105,17 +107,39 @@ func (s *Store) UserByEmail(email string) (*User, error) { func (s *Store) GetUser(id int64) (*User, error) { u := &User{} - err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_image, podcast_author, created_at FROM users WHERE id = ?", id). - Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt) + err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_slug, podcast_image, podcast_author, created_at FROM users WHERE id = ?", id). + Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastSlug, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt) if err != nil { return nil, err } return u, nil } -func (s *Store) UpdateUserPodcast(id int64, title, image, author string) error { - _, err := s.db.Exec("UPDATE users SET podcast_title=?, podcast_image=?, podcast_author=? WHERE id=?", - title, image, author, id) +func (s *Store) UserBySlug(slug string) (*User, error) { + u := &User{} + err := s.db.QueryRow("SELECT id, email, password_hash, podcast_title, podcast_slug, podcast_image, podcast_author, created_at FROM users WHERE podcast_slug = ?", slug). + Scan(&u.ID, &u.Email, &u.PasswordHash, &u.PodcastTitle, &u.PodcastSlug, &u.PodcastImage, &u.PodcastAuthor, &u.CreatedAt) + if err != nil { + return nil, err + } + return u, nil +} + +func (s *Store) SlugTaken(slug string, excludeUserID int64) (bool, error) { + var id int64 + err := s.db.QueryRow("SELECT id FROM users WHERE podcast_slug = ? AND id != ?", slug, excludeUserID).Scan(&id) + if err == sql.ErrNoRows { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + +func (s *Store) UpdateUserPodcast(id int64, title, slug, image, author string) error { + _, err := s.db.Exec("UPDATE users SET podcast_title=?, podcast_slug=?, podcast_image=?, podcast_author=? WHERE id=?", + title, slug, image, author, id) return err } diff --git a/template/dashboard.html b/template/dashboard.html index 7eb7be8..4ed4f66 100644 --- a/template/dashboard.html +++ b/template/dashboard.html @@ -11,11 +11,12 @@ .flash { padding: 0.75rem 1rem; margin-bottom: 1rem; border: 2px solid #000; } .flash-error { background: #ffcdd2; } .flash-success { background: #c8e6c9; } + .flash-info { background: #e3f2fd; } .episode-actions { display: flex; gap: 0.5rem; } .settings-section { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 2px solid #ddd; } .settings-form { display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; } .settings-form .form-group { margin-bottom: 0; } - .inline-form { display: flex; gap: 0.5rem; align-items: center; } + .hidden { display: none; }
@@ -31,6 +32,9 @@