178 lines
5.2 KiB
Go
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)
|
|
}
|