Files

178 lines
5.2 KiB
Go

package main
import (
"encoding/json"
"net/http"
"strconv"
"golang.org/x/crypto/bcrypt"
)
func registerUserRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /api/users", requireAdmin(handleListUsers))
mux.HandleFunc("POST /api/users", requireAuth(handleCreateUser))
mux.HandleFunc("DELETE /api/users/{id}", requireAdmin(handleDeleteUser))
mux.HandleFunc("PATCH /api/users/{id}", requireAdmin(handleAdminUpdateUser))
}
func handleListUsers(w http.ResponseWriter, r *http.Request) {
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
}
defer rows.Close()
out := []User{}
for rows.Next() {
var u User
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)
}
func handleCreateUser(w http.ResponseWriter, r *http.Request) {
actor := userFromCtx(r)
var req struct {
Username string `json:"username"`
Password string `json:"password"`
DisplayName string `json:"display_name"`
Language string `json:"language"`
IsSystemAdmin bool `json:"is_system_admin"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_body")
return
}
req.Username = normalizeUsername(req.Username)
if req.Username == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "missing_fields")
return
}
if len(req.Username) > maxUsernameLen || len(req.DisplayName) > maxDisplayNameLen {
writeError(w, http.StatusBadRequest, "too_long")
return
}
if msg := validatePassword(req.Password); msg != "" {
writeError(w, http.StatusBadRequest, msg)
return
}
if req.IsSystemAdmin && !actor.IsSystemAdmin {
writeError(w, http.StatusForbidden, "forbidden")
return
}
if !actor.IsSystemAdmin {
var chiefCount int
db.QueryRow("SELECT COUNT(*) FROM competition_users WHERE user_id=? AND role='chief_scorer'", actor.ID).Scan(&chiefCount)
if chiefCount == 0 {
writeError(w, http.StatusForbidden, "forbidden")
return
}
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
writeError(w, http.StatusInternalServerError, "hash_error")
return
}
if req.Language == "" {
req.Language = "en"
}
admin := 0
if req.IsSystemAdmin {
admin = 1
}
res, err := db.Exec(
"INSERT INTO users(username,password_hash,display_name,language,is_system_admin) VALUES(?,?,?,?,?)",
req.Username, string(hash), req.DisplayName, req.Language, admin,
)
if err != nil {
writeError(w, http.StatusConflict, "username_taken")
return
}
id, _ := res.LastInsertId()
u, _ := loadUser(id)
writeJSON(w, http.StatusCreated, u)
}
func handleDeleteUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
actor := userFromCtx(r)
if actor.ID == id {
writeError(w, http.StatusBadRequest, "cannot_delete_self")
return
}
if _, err := db.Exec("DELETE FROM users WHERE id=?", id); err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
}
w.WriteHeader(http.StatusNoContent)
}
func handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
var req struct {
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 msg := validatePassword(*req.Password); msg != "" {
writeError(w, http.StatusBadRequest, msg)
return
}
hash, _ := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
db.Exec("UPDATE users SET password_hash=? WHERE id=?", string(hash), id)
}
if req.IsSystemAdmin != nil {
v := 0
if *req.IsSystemAdmin {
v = 1
}
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)
}