package httpapi import ( "encoding/json" "errors" "io" "net/http" "strings" postgresstore "knowledge-governance/internal/platform/postgres" ) // Member administration. Every route here is classified admin by the auth // middleware; these handlers additionally require the session identity. type createUserRequest struct { Username string `json:"username"` DisplayName string `json:"display_name"` Role string `json:"role"` Password string `json:"password"` } func (config Config) handleCreateUser(w http.ResponseWriter, r *http.Request) { identity, ok := sessionFrom(r.Context()) if !ok { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } var input createUserRequest decoder := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid user request"}) return } user, err := config.Auth.Store.CreateUser(r.Context(), strings.TrimSpace(input.Username), strings.TrimSpace(input.DisplayName), input.Role, input.Password, identity.UserID, config.Now().UTC()) switch { case errors.Is(err, postgresstore.ErrAuthInvalidUser): writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username, role or password does not meet policy"}) case errors.Is(err, postgresstore.ErrAuthDuplicateUsername): writeJSON(w, http.StatusConflict, map[string]string{"error": "username already exists"}) case err != nil: writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not create user"}) default: writeJSON(w, http.StatusCreated, map[string]any{"data": userPayload(user)}) } } func (config Config) handleListUsers(w http.ResponseWriter, r *http.Request) { users, err := config.Auth.Store.ListAuthUsers(r.Context()) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not list users"}) return } data := make([]map[string]any, 0, len(users)) for _, user := range users { data = append(data, userPayload(user)) } writeJSON(w, http.StatusOK, map[string]any{"data": data}) } type updateUserRoleRequest struct { Role string `json:"role"` } func (config Config) handleUpdateUserRole(w http.ResponseWriter, r *http.Request) { identity, _ := sessionFrom(r.Context()) var input updateUserRoleRequest decoder := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid role request"}) return } err := config.Auth.Store.SetUserRole(r.Context(), identity.UserID, r.PathValue("id"), input.Role, config.Now().UTC()) switch { case errors.Is(err, postgresstore.ErrAuthInvalidUser): writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid role"}) case errors.Is(err, postgresstore.ErrAuthUserNotFound): writeJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"}) case errors.Is(err, postgresstore.ErrAuthLastAdmin): writeJSON(w, http.StatusConflict, map[string]string{"error": "cannot change the last active admin"}) case err != nil: writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not update role"}) default: user, getErr := config.Auth.Store.GetAuthUserByID(r.Context(), r.PathValue("id")) if getErr != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not load user"}) return } writeJSON(w, http.StatusOK, map[string]any{"data": userPayload(user)}) } } type updateUserStateRequest struct { State string `json:"state"` } func (config Config) handleUpdateUserState(w http.ResponseWriter, r *http.Request) { identity, _ := sessionFrom(r.Context()) var input updateUserStateRequest decoder := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(&input); err != nil || (input.State != "active" && input.State != "disabled") { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "state must be active or disabled"}) return } targetID := r.PathValue("id") if targetID == identity.UserID { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "cannot change your own account state"}) return } err := config.Auth.Store.SetUserState(r.Context(), identity.UserID, targetID, input.State, config.Now().UTC()) switch { case errors.Is(err, postgresstore.ErrAuthUserNotFound): writeJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"}) case errors.Is(err, postgresstore.ErrAuthLastAdmin): writeJSON(w, http.StatusConflict, map[string]string{"error": "cannot disable the last active admin"}) case err != nil: writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not update account"}) default: user, getErr := config.Auth.Store.GetAuthUserByID(r.Context(), targetID) if getErr != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not load user"}) return } writeJSON(w, http.StatusOK, map[string]any{"data": userPayload(user)}) } } type resetUserPasswordRequest struct { TemporaryPassword string `json:"temporary_password"` } func (config Config) handleResetUserPassword(w http.ResponseWriter, r *http.Request) { identity, _ := sessionFrom(r.Context()) var input resetUserPasswordRequest decoder := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(&input); err != nil || len(input.TemporaryPassword) < 8 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "temporary_password must be at least 8 characters"}) return } targetID := r.PathValue("id") if targetID == identity.UserID { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "use change-password for your own account"}) return } err := config.Auth.Store.AdminResetPassword(r.Context(), identity.UserID, targetID, input.TemporaryPassword, config.Now().UTC()) switch { case errors.Is(err, postgresstore.ErrAuthUserNotFound): writeJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"}) case errors.Is(err, postgresstore.ErrAuthUserDisabled): writeJSON(w, http.StatusConflict, map[string]string{"error": "cannot reset a disabled account"}) case errors.Is(err, postgresstore.ErrAuthInvalidUser): writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid temporary password"}) case err != nil: writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not reset password"}) default: writeJSON(w, http.StatusOK, map[string]any{"data": map[string]bool{"reset": true, "must_change_password": true}}) } } func (config Config) userRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/v1/users", config.handleListUsers) mux.HandleFunc("POST /api/v1/users", config.handleCreateUser) mux.HandleFunc("POST /api/v1/users/{id}/role", config.handleUpdateUserRole) mux.HandleFunc("POST /api/v1/users/{id}/state", config.handleUpdateUserState) mux.HandleFunc("POST /api/v1/users/{id}/reset-password", config.handleResetUserPassword) }