package web import ( "context" "net/http" "time" "git.noroutes.net/NoRoutes/vibetunes/internal/auth" "git.noroutes.net/NoRoutes/vibetunes/internal/database" ) func (h *Web) handleAdminRoot(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/admin/users", http.StatusFound) } // --- Users --- func (h *Web) handleAdminUsers(w http.ResponseWriter, r *http.Request) { users, err := database.ListUsers(h.DB) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } h.page(w, r, "admin_users", "admin_users", "Users", users) } func (h *Web) handleAdminUserCreate(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") isAdmin := r.FormValue("is_admin") == "1" if username == "" || len(password) < 8 { http.Redirect(w, r, "/admin/users", http.StatusFound) return } hash, err := auth.HashPassword(password) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } if _, err := database.CreateUser(h.DB, username, "", hash, isAdmin); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin/users", http.StatusFound) } func (h *Web) handleAdminUserToggleDisabled(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") target, err := database.GetUserByID(h.DB, id) if err != nil { http.NotFound(w, r) return } if err := database.SetUserDisabled(h.DB, id, !target.Disabled); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } if !target.Disabled { // just disabled it: revoke sessions _ = database.DeleteUserSessions(h.DB, id) } http.Redirect(w, r, "/admin/users", http.StatusFound) } func (h *Web) handleAdminUserDelete(w http.ResponseWriter, r *http.Request) { current := auth.UserFromContext(r.Context()) id := r.PathValue("id") if id == current.ID { http.Error(w, "cannot delete your own account", http.StatusBadRequest) return } if err := database.DeleteUser(h.DB, id); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin/users", http.StatusFound) } // --- Music folders --- func (h *Web) handleAdminFolders(w http.ResponseWriter, r *http.Request) { folders, err := database.ListMusicFolders(h.DB) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } h.page(w, r, "admin_folders", "admin_folders", "Music Folders", folders) } func (h *Web) handleAdminFolderCreate(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } name := r.FormValue("name") path := r.FormValue("path") kind := r.FormValue("kind") if kind == "" { kind = "music" } if name == "" || path == "" { http.Redirect(w, r, "/admin/folders", http.StatusFound) return } folder, err := database.CreateMusicFolder(h.DB, name, path, kind) if 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, "/admin/folders", http.StatusFound) } func (h *Web) handleAdminFolderToggle(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") folder, err := database.GetMusicFolder(h.DB, id) if err != nil { http.NotFound(w, r) return } if err := database.SetMusicFolderEnabled(h.DB, id, !folder.Enabled); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin/folders", http.StatusFound) } func (h *Web) handleAdminFolderDelete(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := database.DeleteMusicFolder(h.DB, id); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin/folders", http.StatusFound) } func (h *Web) handleAdminFolderScan(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if _, err := database.GetMusicFolder(h.DB, id); err != nil { http.NotFound(w, r) return } if h.Scanner != nil { go func() { _ = h.Scanner.ScanFolder(context.Background(), id) }() } referer := r.Header.Get("Referer") if referer == "" { referer = "/admin/scanner" } http.Redirect(w, r, referer, http.StatusFound) } // --- Scanner status --- type folderScanStatus struct { Folder *database.MusicFolder State *database.ScanState } func (h *Web) handleAdminScanner(w http.ResponseWriter, r *http.Request) { folders, err := database.ListMusicFolders(h.DB) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } var out []folderScanStatus for _, f := range folders { state, err := database.GetScanState(h.DB, f.ID) if err != nil { continue } out = append(out, folderScanStatus{Folder: f, State: state}) } h.page(w, r, "admin_scanner", "admin_scanner", "Scanner", out) } // --- System settings --- func (h *Web) handleAdminSettings(w http.ResponseWriter, r *http.Request) { h.renderAdminSettings(w, r, "", "") } func (h *Web) renderAdminSettings(w http.ResponseWriter, r *http.Request, flash, kind string) { trusted := "" for i, p := range h.Config.ExtAuth.TrustedSources { if i > 0 { trusted += ", " } trusted += p.String() } h.flash(w, r, "admin_settings", "admin_settings", "System Settings", flash, kind, struct { ScanInterval string FFmpegPath string FFprobePath string ExtAuthEnabled bool ExtAuthTrustedSources string ExtAuthHeader string }{ ScanInterval: database.GetSetting(h.DB, "scan_interval", h.Config.ScanInterval.String()), FFmpegPath: database.GetSetting(h.DB, "ffmpeg_path", h.Config.FFmpegPath), FFprobePath: database.GetSetting(h.DB, "ffprobe_path", h.Config.FFprobePath), ExtAuthEnabled: h.Config.ExtAuth.Enabled, ExtAuthTrustedSources: trusted, ExtAuthHeader: h.Config.ExtAuth.UserHeader, }) } func (h *Web) handleAdminSettingsUpdate(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } interval := r.FormValue("scan_interval") if _, err := time.ParseDuration(interval); err != nil { h.renderAdminSettings(w, r, "Invalid scan interval: "+err.Error(), "danger") return } _ = database.SetSetting(h.DB, "scan_interval", interval) _ = database.SetSetting(h.DB, "ffmpeg_path", r.FormValue("ffmpeg_path")) _ = database.SetSetting(h.DB, "ffprobe_path", r.FormValue("ffprobe_path")) h.renderAdminSettings(w, r, "Settings saved. Some changes take effect after a restart.", "success") }