From 6f8503e889dc1f45eddd987ee44e6338712de4fe Mon Sep 17 00:00:00 2001 From: Thomas Karpiniec Date: Wed, 23 Oct 2024 21:55:42 +1100 Subject: [PATCH 1/1] User management --- server/database.go | 9 +++ server/main.go | 137 +++++++++++++++++++++++++++++++++++- server/templates/user.html | 48 +++++++++++++ server/templates/users.html | 8 +++ 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 server/templates/user.html create mode 100644 server/templates/users.html diff --git a/server/database.go b/server/database.go index 1312208..06e5968 100644 --- a/server/database.go +++ b/server/database.go @@ -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") diff --git a/server/main.go b/server/main.go index 5f0a748..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" @@ -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 index 0000000..1118bb6 --- /dev/null +++ b/server/templates/user.html @@ -0,0 +1,48 @@ + +

+ {{if .User.Id}} + Edit User + {{else}} + Add New User + {{end}} +

+
+ +

+ + +

+

+ +
+

+ {{if not .User.Id}} +

+ + +

+ {{end}} +

+ +

+
+ {{if .User.Id}} +

Reset Password

+
+ +

+ + +

+

+ +

+
+

Delete

+
+ +

+ +

+
+ {{end}} diff --git a/server/templates/users.html b/server/templates/users.html new file mode 100644 index 0000000..b95b354 --- /dev/null +++ b/server/templates/users.html @@ -0,0 +1,8 @@ + +

User Management

+ +

Add New User

-- 2.39.5