X-Git-Url: https://code.octet-stream.net/broadcaster/blobdiff_plain/04483e823a42650816c2edbb276709145b9068db..HEAD:/server/main.go diff --git a/server/main.go b/server/main.go index 5f0a748..d1d47d3 100644 --- a/server/main.go +++ b/server/main.go @@ -5,6 +5,7 @@ import ( "embed" "flag" "fmt" + "golang.org/x/crypto/bcrypt" "golang.org/x/net/websocket" "html/template" "io" @@ -76,7 +77,7 @@ func main() { // Public routes http.HandleFunc("/login", logInPage) - http.Handle("/file-downloads/", http.StripPrefix("/file-downloads/", http.FileServer(http.Dir(config.AudioFilesPath)))) + http.Handle("/file-downloads/", applyDisposition(http.StripPrefix("/file-downloads/", http.FileServer(http.Dir(config.AudioFilesPath))))) // Authenticated routes @@ -92,7 +93,7 @@ func main() { // Admin routes - // TODO: user management + http.Handle("/users/", requireAdmin(userSection)) // Websocket routes, which perform their own auth @@ -105,6 +106,24 @@ func main() { } } +type DispositionMiddleware struct { + handler http.Handler +} + +func (m DispositionMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println("path", r.URL.Path) + if r.URL.Path != "/file-downloads/" { + w.Header().Add("Content-Disposition", "attachment") + } + m.handler.ServeHTTP(w, r) +} + +func applyDisposition(handler http.Handler) DispositionMiddleware { + return DispositionMiddleware{ + handler: handler, + } +} + type authenticatedHandler func(http.ResponseWriter, *http.Request, User) type AuthMiddleware struct { @@ -137,14 +156,16 @@ func requireAdmin(handler authenticatedHandler) AuthMiddleware { type HeaderData struct { SelectedMenu string - Username string + User User + Version string } -func renderHeader(w http.ResponseWriter, selectedMenu string) { +func renderHeader(w http.ResponseWriter, selectedMenu string, user User) { tmpl := template.Must(template.ParseFS(content, "templates/header.html")) data := HeaderData{ SelectedMenu: selectedMenu, - Username: "username", + User: user, + Version: version, } err := tmpl.Execute(w, data) if err != nil { @@ -166,7 +187,7 @@ type HomeData struct { } func homePage(w http.ResponseWriter, r *http.Request, user User) { - renderHeader(w, "status") + renderHeader(w, "status", user) tmpl := template.Must(template.ParseFS(content, "templates/index.html")) data := HomeData{ LoggedIn: true, @@ -199,7 +220,7 @@ func logInPage(w http.ResponseWriter, r *http.Request) { data := LogInData{ Error: errText, } - renderHeader(w, "") + renderHeader(w, "", User{}) tmpl := template.Must(template.ParseFS(content, "templates/login.html")) tmpl.Execute(w, data) renderFooter(w) @@ -212,20 +233,20 @@ func playlistSection(w http.ResponseWriter, r *http.Request, user User) { return } if path[2] == "new" { - editPlaylistPage(w, r, 0) + editPlaylistPage(w, r, 0, user) } else if path[2] == "submit" && r.Method == "POST" { submitPlaylist(w, r) } else if path[2] == "delete" && r.Method == "POST" { deletePlaylist(w, r) } else if path[2] == "" { - playlistsPage(w, r) + playlistsPage(w, r, user) } else { id, err := strconv.Atoi(path[2]) if err != nil { http.NotFound(w, r) return } - editPlaylistPage(w, r, id) + editPlaylistPage(w, r, id, user) } } @@ -240,7 +261,7 @@ func fileSection(w http.ResponseWriter, r *http.Request, user User) { } else if path[2] == "delete" && r.Method == "POST" { deleteFile(w, r) } else if path[2] == "" { - filesPage(w, r) + filesPage(w, r, user) } else { http.NotFound(w, r) return @@ -254,21 +275,138 @@ func radioSection(w http.ResponseWriter, r *http.Request, user User) { return } if path[2] == "new" { - editRadioPage(w, r, 0) + editRadioPage(w, r, 0, user) } else if path[2] == "submit" && r.Method == "POST" { submitRadio(w, r) } else if path[2] == "delete" && r.Method == "POST" { deleteRadio(w, r) } else if path[2] == "" { - radiosPage(w, r) + radiosPage(w, r, user) + } else { + id, err := strconv.Atoi(path[2]) + if err != nil { + http.NotFound(w, r) + return + } + editRadioPage(w, r, id, user) + } +} + +func userSection(w http.ResponseWriter, r *http.Request, user User) { + path := strings.Split(r.URL.Path, "/") + if len(path) != 3 { + http.NotFound(w, r) + return + } + if path[2] == "new" { + editUserPage(w, r, 0, user) + } else if path[2] == "submit" && r.Method == "POST" { + submitUser(w, r) + } else if path[2] == "delete" && r.Method == "POST" { + deleteUser(w, r) + } else if path[2] == "reset-password" && r.Method == "POST" { + resetUserPassword(w, r) + } else if path[2] == "" { + usersPage(w, r, user) } else { id, err := strconv.Atoi(path[2]) if err != nil { http.NotFound(w, r) return } - editRadioPage(w, r, id) + editUserPage(w, r, id, user) + } +} + +type EditUserPageData struct { + User User +} + +func editUserPage(w http.ResponseWriter, r *http.Request, id int, user User) { + var data EditUserPageData + if id != 0 { + user, err := db.GetUserById(id) + if err != nil { + http.NotFound(w, r) + return + } + data.User = user } + renderHeader(w, "users", user) + tmpl := template.Must(template.ParseFS(content, "templates/user.html")) + tmpl.Execute(w, data) + renderFooter(w) +} + +func submitUser(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err == nil { + id, err := strconv.Atoi(r.Form.Get("userId")) + if err != nil { + return + } + if id == 0 { + newPassword := r.Form.Get("password") + hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return + } + user := User{ + Id: 0, + Username: r.Form.Get("username"), + IsAdmin: r.Form.Get("isAdmin") == "1", + PasswordHash: string(hashed), + } + db.CreateUser(user) + } else { + user, err := db.GetUserById(id) + if err != nil { + http.NotFound(w, r) + return + } + db.SetUserIsAdmin(user.Username, r.Form.Get("isAdmin") == "1") + } + } + http.Redirect(w, r, "/users/", http.StatusFound) +} + +func deleteUser(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err == nil { + id, err := strconv.Atoi(r.Form.Get("userId")) + if err != nil { + return + } + user, err := db.GetUserById(id) + if err != nil { + http.NotFound(w, r) + return + } + db.DeleteUser(user.Username) + } + http.Redirect(w, r, "/users/", http.StatusFound) +} + +func resetUserPassword(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err == nil { + id, err := strconv.Atoi(r.Form.Get("userId")) + if err != nil { + return + } + user, err := db.GetUserById(id) + if err != nil { + http.NotFound(w, r) + return + } + newPassword := r.Form.Get("newPassword") + hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return + } + db.SetUserPassword(user.Username, string(hashed)) + } + http.Redirect(w, r, "/users/", http.StatusFound) } type ChangePasswordPageData struct { @@ -295,7 +433,7 @@ func changePasswordPage(w http.ResponseWriter, r *http.Request, user User) { data.ShowForm = false cookie, err := r.Cookie("broadcast_session") if err == nil { - log.Println("clearing other sessions for username", user.Username, "token", cookie.Value) + log.Println("Clearing other sessions for username", user.Username, "token", cookie.Value) db.ClearOtherSessions(user.Username, cookie.Value) } } @@ -303,7 +441,7 @@ func changePasswordPage(w http.ResponseWriter, r *http.Request, user User) { data.Message = "" data.ShowForm = true } - renderHeader(w, "change-password") + renderHeader(w, "change-password", user) tmpl := template.Must(template.ParseFS(content, "templates/change_password.html")) err := tmpl.Execute(w, data) if err != nil { @@ -312,15 +450,35 @@ func changePasswordPage(w http.ResponseWriter, r *http.Request, user User) { renderFooter(w) } +type UsersPageData struct { + Users []User +} + +func usersPage(w http.ResponseWriter, _ *http.Request, user User) { + renderHeader(w, "users", user) + data := UsersPageData{ + Users: db.GetUsers(), + } + tmpl := template.Must(template.ParseFS(content, "templates/users.html")) + err := tmpl.Execute(w, data) + if err != nil { + log.Fatal(err) + } + renderFooter(w) +} + type PlaylistsPageData struct { Playlists []Playlist } -func playlistsPage(w http.ResponseWriter, _ *http.Request) { - renderHeader(w, "playlists") +func playlistsPage(w http.ResponseWriter, _ *http.Request, user User) { + renderHeader(w, "playlists", user) data := PlaylistsPageData{ Playlists: db.GetPlaylists(), } + for i := range data.Playlists { + data.Playlists[i].StartTime = strings.Replace(data.Playlists[i].StartTime, "T", " ", -1) + } tmpl := template.Must(template.ParseFS(content, "templates/playlists.html")) err := tmpl.Execute(w, data) if err != nil { @@ -333,8 +491,8 @@ type RadiosPageData struct { Radios []Radio } -func radiosPage(w http.ResponseWriter, _ *http.Request) { - renderHeader(w, "radios") +func radiosPage(w http.ResponseWriter, _ *http.Request, user User) { + renderHeader(w, "radios", user) data := RadiosPageData{ Radios: db.GetRadios(), } @@ -352,7 +510,7 @@ type EditPlaylistPageData struct { Files []string } -func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) { +func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int, user User) { var data EditPlaylistPageData for _, f := range files.Files() { data.Files = append(data.Files, f.Name) @@ -371,7 +529,7 @@ func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) { data.Playlist = playlist data.Entries = db.GetEntriesForPlaylist(id) } - renderHeader(w, "radios") + renderHeader(w, "playlists", user) tmpl := template.Must(template.ParseFS(content, "templates/playlist.html")) tmpl.Execute(w, data) renderFooter(w) @@ -447,7 +605,7 @@ type EditRadioPageData struct { Radio Radio } -func editRadioPage(w http.ResponseWriter, r *http.Request, id int) { +func editRadioPage(w http.ResponseWriter, r *http.Request, id int, user User) { var data EditRadioPageData if id == 0 { data.Radio.Name = "New Radio" @@ -460,7 +618,7 @@ func editRadioPage(w http.ResponseWriter, r *http.Request, id int) { } data.Radio = radio } - renderHeader(w, "radios") + renderHeader(w, "radios", user) tmpl := template.Must(template.ParseFS(content, "templates/radio.html")) tmpl.Execute(w, data) renderFooter(w) @@ -502,12 +660,11 @@ type FilesPageData struct { Files []FileSpec } -func filesPage(w http.ResponseWriter, _ *http.Request) { - renderHeader(w, "files") +func filesPage(w http.ResponseWriter, _ *http.Request, user User) { + renderHeader(w, "files", user) data := FilesPageData{ Files: files.Files(), } - log.Println("file page data", data) tmpl := template.Must(template.ParseFS(content, "templates/files.html")) err := tmpl.Execute(w, data) if err != nil { @@ -536,15 +693,19 @@ func uploadFile(w http.ResponseWriter, r *http.Request) { f, _ := os.Create(path) defer f.Close() io.Copy(f, file) - log.Println("uploaded file to", path) + log.Println("Uploaded file to", path) files.Refresh() } http.Redirect(w, r, "/files/", http.StatusFound) } func logOutPage(w http.ResponseWriter, r *http.Request, user User) { + cookie, err := r.Cookie("broadcast_session") + if err == nil { + db.ClearSession(user.Username, cookie.Value) + } clearSessionCookie(w) - renderHeader(w, "") + renderHeader(w, "", user) tmpl := template.Must(template.ParseFS(content, "templates/logout.html")) tmpl.Execute(w, nil) renderFooter(w)