]> code.octet-stream.net Git - broadcaster/commitdiff
User management
authorThomas Karpiniec <tom.karpiniec@outlook.com>
Wed, 23 Oct 2024 10:55:42 +0000 (21:55 +1100)
committerThomas Karpiniec <tom.karpiniec@outlook.com>
Wed, 23 Oct 2024 10:55:42 +0000 (21:55 +1100)
server/database.go
server/main.go
server/templates/user.html [new file with mode: 0644]
server/templates/users.html [new file with mode: 0644]

index 13122083f94c4b140dac3ff65bedff72d22b04f6..06e5968ac1090b2e8cbaad4857338b8004ac0b0f 100644 (file)
@@ -79,6 +79,15 @@ func (d *Database) GetUser(username string) (User, error) {
        return user, nil
 }
 
+func (d *Database) GetUserById(id int) (User, error) {
+       var user User
+       err := d.sqldb.QueryRow("SELECT id, username, password_hash, is_admin FROM users WHERE id = ?", id).Scan(&user.Id, &user.Username, &user.PasswordHash, &user.IsAdmin)
+       if err != nil {
+               return User{}, errors.New("no user with that id")
+       }
+       return user, nil
+}
+
 func (d *Database) GetUsers() []User {
        ret := make([]User, 0)
        rows, err := d.sqldb.Query("SELECT id, username, password_hash, is_admin FROM users ORDER BY username ASC")
index 5f0a7487c3aff8e0082588386f0f32abc745ae3b..fe09ea29a6cac1caceb8b7196a3f552e3f030f1e 100644 (file)
@@ -5,6 +5,7 @@ import (
        "embed"
        "flag"
        "fmt"
+       "golang.org/x/crypto/bcrypt"
        "golang.org/x/net/websocket"
        "html/template"
        "io"
@@ -92,7 +93,7 @@ func main() {
 
        // Admin routes
 
-       // TODO: user management
+       http.Handle("/users/", requireAdmin(userSection))
 
        // Websocket routes, which perform their own auth
 
@@ -271,6 +272,123 @@ func radioSection(w http.ResponseWriter, r *http.Request, user 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)
+       } 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
@@ -312,6 +430,23 @@ func changePasswordPage(w http.ResponseWriter, r *http.Request, user User) {
        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)
+       }
+       renderFooter(w)
+}
+
 type PlaylistsPageData struct {
        Playlists []Playlist
 }
diff --git a/server/templates/user.html b/server/templates/user.html
new file mode 100644 (file)
index 0000000..1118bb6
--- /dev/null
@@ -0,0 +1,48 @@
+
+      <h2>
+      {{if .User.Id}}
+      Edit User
+      {{else}}
+      Add New User
+      {{end}}
+      </h2>
+      <form action="/users/submit" method="POST">
+        <input type="hidden" name="userId" value="{{.User.Id}}">
+        <p>
+        <label for="username">Username:</label>
+        <input type="text" id="username" name="username" value="{{.User.Username}}" {{if .User.Id}} disabled {{end}}>
+        </p>
+        <p>
+        <input type="checkbox" id="isAdmin" name="isAdmin" value="1" {{if .User.IsAdmin}} checked {{end}}>
+        <label for="isAdmin">Is an administrator - can manage system users</label><br>
+        </p>
+        {{if not .User.Id}}
+        <p>
+        <label for="password">Password:</label>
+        <input type="password" id="password" name="password">
+        </p>
+        {{end}}
+        <p>
+        <input type="submit" value="Save User">
+        </p>
+      </form>
+      {{if .User.Id}}
+      <h3>Reset Password</h3>
+      <form action="/users/reset-password" method="POST">
+        <input type="hidden" name="userId" value="{{.User.Id}}">
+        <p>
+        <label for="newPassword">New Password:</label>
+        <input type="password" id="newPassword" name="newPassword">
+        </p>
+        <p>
+        <input type="submit" value="Reset Password">
+        </p>
+      </form>
+      <h3>Delete</h3>
+      <form action="/users/delete" method="POST">
+        <input type="hidden" name="userId" value="{{.User.Id}}">
+        <p>
+        <input type="submit" value="Delete User">
+        </p>
+      </form>
+      {{end}}
diff --git a/server/templates/users.html b/server/templates/users.html
new file mode 100644 (file)
index 0000000..b95b354
--- /dev/null
@@ -0,0 +1,8 @@
+
+      <h1>User Management</h1>
+      <ul>
+      {{range .Users}}
+        <li><b>{{.Username}}</b> <a href="/users/{{.Id}}">(Edit)</a></li>
+      {{end}}
+      </ul>
+      <p><a href="/users/new">Add New User</a></p>