X-Git-Url: https://code.octet-stream.net/broadcaster/blobdiff_plain/7b615b3c71825b5b229b78509a16db37e1d3f38d..6f8503e889dc1f45eddd987ee44e6338712de4fe:/server/main.go diff --git a/server/main.go b/server/main.go index 8f76cbf..fe09ea2 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" @@ -22,6 +23,7 @@ const formatString = "2006-01-02T15:04" //go:embed templates/* var content embed.FS + //var content = os.DirFS("../broadcaster-server/") var config ServerConfig = NewServerConfig() @@ -79,36 +81,71 @@ func main() { // Authenticated routes - http.HandleFunc("/", homePage) - http.HandleFunc("/logout", logOutPage) - http.HandleFunc("/change-password", changePasswordPage) + http.Handle("/", requireUser(homePage)) + http.Handle("/logout", requireUser(logOutPage)) + http.Handle("/change-password", requireUser(changePasswordPage)) - http.HandleFunc("/playlists/", playlistSection) - http.HandleFunc("/files/", fileSection) - http.HandleFunc("/radios/", radioSection) + http.Handle("/playlists/", requireUser(playlistSection)) + http.Handle("/files/", requireUser(fileSection)) + http.Handle("/radios/", requireUser(radioSection)) - http.Handle("/radio-ws", websocket.Handler(RadioSync)) - http.Handle("/web-ws", websocket.Handler(WebSync)) - http.HandleFunc("/stop", stopPage) + http.Handle("/stop", requireUser(stopPage)) // Admin routes + http.Handle("/users/", requireAdmin(userSection)) + + // Websocket routes, which perform their own auth + + http.Handle("/radio-ws", websocket.Handler(RadioSync)) + http.Handle("/web-ws", websocket.Handler(WebSync)) + err := http.ListenAndServe(config.BindAddress+":"+strconv.Itoa(config.Port), nil) if err != nil { log.Fatal(err) } } +type authenticatedHandler func(http.ResponseWriter, *http.Request, User) + +type AuthMiddleware struct { + handler authenticatedHandler + mustBeAdmin bool +} + +func (m AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + user, err := currentUser(w, r) + if err != nil || (m.mustBeAdmin && !user.IsAdmin) { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + m.handler(w, r, user) +} + +func requireUser(handler authenticatedHandler) AuthMiddleware { + return AuthMiddleware{ + handler: handler, + mustBeAdmin: false, + } +} + +func requireAdmin(handler authenticatedHandler) AuthMiddleware { + return AuthMiddleware{ + handler: handler, + mustBeAdmin: true, + } +} + type HeaderData struct { SelectedMenu string - Username string + Username string } func renderHeader(w http.ResponseWriter, selectedMenu string) { tmpl := template.Must(template.ParseFS(content, "templates/header.html")) data := HeaderData{ SelectedMenu: selectedMenu, - Username: "username", + Username: "username", } err := tmpl.Execute(w, data) if err != nil { @@ -129,7 +166,7 @@ type HomeData struct { Username string } -func homePage(w http.ResponseWriter, r *http.Request) { +func homePage(w http.ResponseWriter, r *http.Request, user User) { renderHeader(w, "status") tmpl := template.Must(template.ParseFS(content, "templates/index.html")) data := HomeData{ @@ -145,7 +182,6 @@ type LogInData struct { } func logInPage(w http.ResponseWriter, r *http.Request) { - log.Println("Log in page!") r.ParseForm() username := r.Form["username"] password := r.Form["password"] @@ -170,7 +206,7 @@ func logInPage(w http.ResponseWriter, r *http.Request) { renderFooter(w) } -func playlistSection(w http.ResponseWriter, r *http.Request) { +func playlistSection(w http.ResponseWriter, r *http.Request, user User) { path := strings.Split(r.URL.Path, "/") if len(path) != 3 { http.NotFound(w, r) @@ -194,7 +230,7 @@ func playlistSection(w http.ResponseWriter, r *http.Request) { } } -func fileSection(w http.ResponseWriter, r *http.Request) { +func fileSection(w http.ResponseWriter, r *http.Request, user User) { path := strings.Split(r.URL.Path, "/") if len(path) != 3 { http.NotFound(w, r) @@ -212,7 +248,7 @@ func fileSection(w http.ResponseWriter, r *http.Request) { } } -func radioSection(w http.ResponseWriter, r *http.Request) { +func radioSection(w http.ResponseWriter, r *http.Request, user User) { path := strings.Split(r.URL.Path, "/") if len(path) != 3 { http.NotFound(w, r) @@ -236,17 +272,129 @@ func radioSection(w http.ResponseWriter, r *http.Request) { } } +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) + } 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) + } else { + id, err := strconv.Atoi(path[2]) + if err != nil { + http.NotFound(w, r) + return + } + editUserPage(w, r, id) + } +} + +type EditUserPageData struct { + User User +} + +func editUserPage(w http.ResponseWriter, r *http.Request, id int) { + 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") + 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 { Message string ShowForm bool } -func changePasswordPage(w http.ResponseWriter, r *http.Request) { - user, err := currentUser(w, r) - if err != nil { - http.Redirect(w, r, "/login", http.StatusFound) - return - } +func changePasswordPage(w http.ResponseWriter, r *http.Request, user User) { var data ChangePasswordPageData if r.Method == "POST" { err := r.ParseForm() @@ -275,7 +423,24 @@ func changePasswordPage(w http.ResponseWriter, r *http.Request) { } renderHeader(w, "change-password") tmpl := template.Must(template.ParseFS(content, "templates/change_password.html")) - err = tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + log.Fatal(err) + } + renderFooter(w) +} + +type UsersPageData struct { + Users []User +} + +func usersPage(w http.ResponseWriter, _ *http.Request) { + renderHeader(w, "users") + 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) } @@ -512,7 +677,7 @@ func uploadFile(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/files/", http.StatusFound) } -func logOutPage(w http.ResponseWriter, r *http.Request) { +func logOutPage(w http.ResponseWriter, r *http.Request, user User) { clearSessionCookie(w) renderHeader(w, "") tmpl := template.Must(template.ParseFS(content, "templates/logout.html")) @@ -520,12 +685,7 @@ func logOutPage(w http.ResponseWriter, r *http.Request) { renderFooter(w) } -func stopPage(w http.ResponseWriter, r *http.Request) { - _, err := currentUser(w, r) - if err != nil { - http.Redirect(w, r, "/login", http.StatusFound) - return - } +func stopPage(w http.ResponseWriter, r *http.Request, user User) { r.ParseForm() radioId, err := strconv.Atoi(r.Form.Get("radioId")) if err != nil {