]> code.octet-stream.net Git - broadcaster/blobdiff - server/main.go
User management
[broadcaster] / server / main.go
index 8f76cbfacff9d9afb601c6b762d320203bc34daf..105e8581c077474241d2a8e7cbdd65bac7e6dc86 100644 (file)
@@ -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
+       User         User
 }
 
-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,
        }
        err := tmpl.Execute(w, data)
        if err != nil {
@@ -129,8 +166,8 @@ type HomeData struct {
        Username string
 }
 
-func homePage(w http.ResponseWriter, r *http.Request) {
-       renderHeader(w, "status")
+func homePage(w http.ResponseWriter, r *http.Request, user User) {
+       renderHeader(w, "status", user)
        tmpl := template.Must(template.ParseFS(content, "templates/index.html"))
        data := HomeData{
                LoggedIn: true,
@@ -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"]
@@ -164,37 +200,37 @@ 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)
 }
 
-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)
                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)
        }
 }
 
-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)
@@ -205,35 +241,152 @@ func fileSection(w http.ResponseWriter, r *http.Request) {
        } 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
        }
 }
 
-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)
                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)
+               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
+               }
+               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 {
@@ -241,12 +394,7 @@ type ChangePasswordPageData struct {
        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()
@@ -273,9 +421,26 @@ func changePasswordPage(w http.ResponseWriter, r *http.Request) {
                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)
+       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, 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)
        }
@@ -286,8 +451,8 @@ 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(),
        }
@@ -303,8 +468,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(),
        }
@@ -322,7 +487,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)
@@ -341,7 +506,7 @@ func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) {
                data.Playlist = playlist
                data.Entries = db.GetEntriesForPlaylist(id)
        }
-       renderHeader(w, "radios")
+       renderHeader(w, "radios", user)
        tmpl := template.Must(template.ParseFS(content, "templates/playlist.html"))
        tmpl.Execute(w, data)
        renderFooter(w)
@@ -417,7 +582,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"
@@ -430,7 +595,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)
@@ -472,8 +637,8 @@ 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(),
        }
@@ -512,20 +677,15 @@ 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, "")
+       renderHeader(w, "", user)
        tmpl := template.Must(template.ParseFS(content, "templates/logout.html"))
        tmpl.Execute(w, nil)
        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 {