Web UI: setup wizard, auth pages, library browsing, player, admin, streaming, cover art
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user