Replaced one-pager with multiple pages and fixed security bugs

This commit is contained in:
Jan Meinl
2026-05-16 21:10:55 +02:00
parent 802906f9d4
commit 68034dea7d
25 changed files with 2311 additions and 1217 deletions
+41 -8
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"net/http"
"strconv"
"strings"
"golang.org/x/crypto/bcrypt"
)
@@ -17,7 +16,7 @@ func registerUserRoutes(mux *http.ServeMux) {
}
func handleListUsers(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id,username,display_name,language,is_system_admin FROM users ORDER BY username")
rows, err := db.Query("SELECT id,username,display_name,language,is_system_admin,must_change_password FROM users ORDER BY username")
if err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
@@ -26,9 +25,10 @@ func handleListUsers(w http.ResponseWriter, r *http.Request) {
out := []User{}
for rows.Next() {
var u User
var admin int
rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Language, &admin)
var admin, mustChange int
rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Language, &admin, &mustChange)
u.IsSystemAdmin = admin == 1
u.MustChangePassword = mustChange == 1
out = append(out, u)
}
writeJSON(w, http.StatusOK, out)
@@ -47,11 +47,16 @@ func handleCreateUser(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "invalid_body")
return
}
req.Username = strings.TrimSpace(req.Username)
req.Username = normalizeUsername(req.Username)
if req.Username == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "missing_fields")
return
}
if len(req.Username) > maxUsernameLen || len(req.Password) > maxPasswordLen ||
len(req.DisplayName) > maxDisplayNameLen {
writeError(w, http.StatusBadRequest, "too_long")
return
}
if req.IsSystemAdmin && !actor.IsSystemAdmin {
writeError(w, http.StatusForbidden, "forbidden")
return
@@ -114,18 +119,39 @@ func handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) {
return
}
var req struct {
DisplayName *string `json:"display_name"`
Password *string `json:"password"`
IsSystemAdmin *bool `json:"is_system_admin"`
Username *string `json:"username"`
DisplayName *string `json:"display_name"`
Password *string `json:"password"`
IsSystemAdmin *bool `json:"is_system_admin"`
MustChangePassword *bool `json:"must_change_password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_body")
return
}
if req.Username != nil {
newName := normalizeUsername(*req.Username)
if newName == "" || len(newName) > maxUsernameLen {
writeError(w, http.StatusBadRequest, "invalid_username")
return
}
if _, err := db.Exec("UPDATE users SET username=? WHERE id=?", newName, id); err != nil {
writeError(w, http.StatusConflict, "username_taken")
return
}
}
if req.DisplayName != nil {
if len(*req.DisplayName) > maxDisplayNameLen {
writeError(w, http.StatusBadRequest, "too_long")
return
}
db.Exec("UPDATE users SET display_name=? WHERE id=?", *req.DisplayName, id)
}
if req.Password != nil && *req.Password != "" {
if len(*req.Password) > maxPasswordLen {
writeError(w, http.StatusBadRequest, "too_long")
return
}
hash, _ := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
db.Exec("UPDATE users SET password_hash=? WHERE id=?", string(hash), id)
}
@@ -136,6 +162,13 @@ func handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) {
}
db.Exec("UPDATE users SET is_system_admin=? WHERE id=?", v, id)
}
if req.MustChangePassword != nil {
v := 0
if *req.MustChangePassword {
v = 1
}
db.Exec("UPDATE users SET must_change_password=? WHERE id=?", v, id)
}
u, _ := loadUser(id)
writeJSON(w, http.StatusOK, u)
}