Web UI: setup wizard, auth pages, library browsing, player, admin, streaming, cover art

This commit is contained in:
2026-08-22 23:42:09 -05:00
parent aa29f13846
commit e0bd8bc3cb
42 changed files with 3012 additions and 8 deletions
+176
View File
@@ -0,0 +1,176 @@
package web
import (
"context"
"net/http"
"os/exec"
"git.noroutes.net/NoRoutes/vibetunes/internal/auth"
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
)
func (h *Web) handleLoginPage(w http.ResponseWriter, r *http.Request) {
if h.Sessions.CurrentUser(r) != nil {
http.Redirect(w, r, "/", http.StatusFound)
return
}
render(w, "login", PageData{
Title: "Log in",
CSRFToken: h.CSRF.Token(w, r),
Data: struct{ Next string }{Next: r.URL.Query().Get("next")},
})
}
func (h *Web) handleLogin(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
next := r.FormValue("next")
user, err := database.GetUserByUsername(h.DB, username)
if err != nil || user.Disabled || !user.PasswordHash.Valid {
h.loginFailed(w, r, next)
return
}
ok, err := auth.VerifyPassword(password, user.PasswordHash.String)
if err != nil || !ok {
h.loginFailed(w, r, next)
return
}
if err := h.Sessions.Issue(w, r, user.ID); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
_ = database.TouchLastLogin(h.DB, user.ID)
if next == "" || next[0] != '/' {
next = "/"
}
http.Redirect(w, r, next, http.StatusFound)
}
func (h *Web) loginFailed(w http.ResponseWriter, r *http.Request, next string) {
render(w, "login", PageData{
Title: "Log in",
CSRFToken: h.CSRF.Token(w, r),
Flash: "Invalid username or password",
FlashKind: "danger",
Data: struct{ Next string }{Next: next},
})
}
func (h *Web) handleLogout(w http.ResponseWriter, r *http.Request) {
h.Sessions.Clear(w, r)
http.Redirect(w, r, "/login", http.StatusFound)
}
// --- First-run setup ---
func (h *Web) handleSetupPage(w http.ResponseWriter, r *http.Request) {
n, err := database.CountUsers(h.DB)
if err == nil && n > 0 {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
render(w, "setup", PageData{
Title: "Setup",
CSRFToken: h.CSRF.Token(w, r),
Data: h.setupData(),
})
}
func (h *Web) setupData() any {
ffmpegPath := resolveOr(h.Config.FFmpegPath, "ffmpeg")
ffprobePath := resolveOr(h.Config.FFprobePath, "ffprobe")
_, ffmpegErr := exec.LookPath(ffmpegPath)
_, ffprobeErr := exec.LookPath(ffprobePath)
return struct {
FFmpegPath string
FFmpegFound bool
FFprobePath string
FFprobeFound bool
}{
FFmpegPath: ffmpegPath,
FFmpegFound: ffmpegErr == nil,
FFprobePath: ffprobePath,
FFprobeFound: ffprobeErr == nil,
}
}
func resolveOr(configured, def string) string {
if configured != "" {
return configured
}
return def
}
func (h *Web) handleSetup(w http.ResponseWriter, r *http.Request) {
n, err := database.CountUsers(h.DB)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if n > 0 {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
folderName := r.FormValue("folder_name")
folderPath := r.FormValue("folder_path")
if username == "" || len(password) < 8 || folderPath == "" {
render(w, "setup", PageData{
Title: "Setup", CSRFToken: h.CSRF.Token(w, r),
Flash: "Please fill in all fields (password must be at least 8 characters)", FlashKind: "danger",
Data: h.setupData(),
})
return
}
hash, err := auth.HashPassword(password)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
user, err := database.CreateUser(h.DB, username, "", hash, true)
if err != nil {
render(w, "setup", PageData{
Title: "Setup", CSRFToken: h.CSRF.Token(w, r),
Flash: "Could not create the administrator account: " + err.Error(), FlashKind: "danger",
Data: h.setupData(),
})
return
}
folder, err := database.CreateMusicFolder(h.DB, folderName, folderPath, "music")
if err != nil {
render(w, "setup", PageData{
Title: "Setup", CSRFToken: h.CSRF.Token(w, r),
Flash: "Administrator account created, but the music folder could not be added: " + err.Error(), FlashKind: "warning",
Data: h.setupData(),
})
return
}
if err := h.Sessions.Issue(w, r, user.ID); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if h.Scanner != nil {
go func() {
_ = h.Scanner.ScanFolder(context.Background(), folder.ID)
}()
}
http.Redirect(w, r, "/", http.StatusFound)
}