Ctrl K
ComunidadGuías…

codersdk/users.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package codersdk

import (
	"context"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
	"golang.org/x/xerrors"
)

// Me is used as a replacement for your own ID.
var Me = "me"

type UserStatus string

const (
	UserStatusActive    UserStatus = "active"
	UserStatusDormant   UserStatus = "dormant"
	UserStatusSuspended UserStatus = "suspended"
)

type UsersRequest struct {
	Search string `json:"search,omitempty" typescript:"-"`
	Name   string `json:"name,omitempty" typescript:"-"`
	// Filter users by status.
	Status UserStatus `json:"status,omitempty" typescript:"-"`
	// Filter users that have the given role.
	Role      string      `json:"role,omitempty" typescript:"-"`
	LoginType []LoginType `json:"login_type,omitempty" typescript:"-"`

	SearchQuery string `json:"q,omitempty"`
	Pagination
}

func (req UsersRequest) asRequestOption() RequestOption {
	return func(r *http.Request) {
		q := r.URL.Query()
		var params []string
		if req.Search != "" {
			params = append(params, req.Search)
		}
		if req.Name != "" {
			params = append(params, "name:"+req.Name)
		}
		if req.Status != "" {
			params = append(params, "status:"+string(req.Status))
		}
		if req.Role != "" {
			params = append(params, "role:"+req.Role)
		}
		if req.SearchQuery != "" {
			params = append(params, req.SearchQuery)
		}
		for _, lt := range req.LoginType {
			params = append(params, "login_type:"+string(lt))
		}
		q.Set("q", strings.Join(params, " "))
		r.URL.RawQuery = q.Encode()
	}
}

// MinimalUser is the minimal information needed to identify a user and show
// them on the UI.
type MinimalUser struct {
	ID        uuid.UUID `json:"id" validate:"required" table:"id" format:"uuid"`
	Username  string    `json:"username" validate:"required" table:"username,default_sort"`
	Name      string    `json:"name,omitempty" table:"name"`
	AvatarURL string    `json:"avatar_url,omitempty" format:"uri"`
}

// ReducedUser omits role and organization information. Roles are deduced from
// the user's site and organization roles. This requires fetching the user's
// organizational memberships. Fetching that is more expensive, and not usually
// required by the frontend.
type ReducedUser struct {
	MinimalUser `table:"m,recursive_inline"`
	Email       string    `json:"email" validate:"required" table:"email" format:"email"`
	CreatedAt   time.Time `json:"created_at" validate:"required" table:"created at" format:"date-time"`
	UpdatedAt   time.Time `json:"updated_at" table:"updated at" format:"date-time"`
	LastSeenAt  time.Time `json:"last_seen_at,omitempty" format:"date-time"`

	Status           UserStatus `json:"status" table:"status"`
	LoginType        LoginType  `json:"login_type"`
	IsServiceAccount bool       `json:"is_service_account,omitempty"`
	// Deprecated: this value should be retrieved from
	// `codersdk.UserPreferenceSettings` instead.
	ThemePreference string `json:"theme_preference,omitempty"`
}

// User represents a user in Coder.
type User struct {
	ReducedUser `table:"r,recursive_inline"`

	OrganizationIDs []uuid.UUID `json:"organization_ids" format:"uuid"`
	Roles           []SlimRole  `json:"roles"`
	// HasAISeat intentionally omits omitempty so the API always includes the
	// field, even when false.
	HasAISeat bool `json:"has_ai_seat"`
}

type GetUsersResponse struct {
	Users []User `json:"users"`
	Count int    `json:"count"`
}

// Trial request source origination reported to the licensor.
const (
	// LicensorTrialSourceNewUser is the first user setup flow.
	LicensorTrialSourceNewUser = "NewUser"
	// LicensorTrialSourceProduct is a request from within the product in-app trial request
	LicensorTrialSourceProduct = "Product"
)

// @typescript-ignore LicensorTrialRequest
type LicensorTrialRequest struct {
	DeploymentID string `json:"deployment_id"`
	Email        string `json:"email"`
	Source       string `json:"source"`

	// Personal details.
	FirstName   string `json:"first_name"`
	LastName    string `json:"last_name"`
	PhoneNumber string `json:"phone_number"`
	JobTitle    string `json:"job_title"`
	CompanyName string `json:"company_name"`
	Country     string `json:"country"`
	Developers  string `json:"developers"`
}

type CreateFirstUserRequest struct {
	Email          string                         `json:"email" validate:"required,email"`
	Username       string                         `json:"username" validate:"required,username"`
	Name           string                         `json:"name" validate:"user_real_name"`
	Password       string                         `json:"password" validate:"required"`
	Trial          bool                           `json:"trial"`
	TrialInfo      CreateFirstUserTrialInfo       `json:"trial_info"`
	OnboardingInfo *CreateFirstUserOnboardingInfo `json:"onboarding_info,omitempty"`
}

type CreateFirstUserTrialInfo struct {
	FirstName   string `json:"first_name"`
	LastName    string `json:"last_name"`
	PhoneNumber string `json:"phone_number"`
	JobTitle    string `json:"job_title"`
	CompanyName string `json:"company_name"`
	Country     string `json:"country"`
	Developers  string `json:"developers"`
}

// CreateFirstUserOnboardingInfo contains optional newsletter preference
// data collected during first user setup.
type CreateFirstUserOnboardingInfo struct {
	NewsletterMarketing bool `json:"newsletter_marketing"`
	NewsletterReleases  bool `json:"newsletter_releases"`
}

// CreateFirstUserResponse contains IDs for newly created user info.
type CreateFirstUserResponse struct {
	UserID         uuid.UUID `json:"user_id" format:"uuid"`
	OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
}

type CreateUserRequest struct {
	Email    string `json:"email" validate:"required_unless=ServiceAccount true,omitempty,email" format:"email"`
	Username string `json:"username" validate:"required,username"`
	Name     string `json:"name" validate:"user_real_name"`
	Password string `json:"password"`
	// UserLoginType defaults to LoginTypePassword.
	UserLoginType LoginType `json:"login_type"`
	// UserStatus defaults to UserStatusDormant. Service accounts default to
	// UserStatusActive and can't be dormant.
	UserStatus *UserStatus `json:"user_status"`
	// OrganizationIDs is a list of organization IDs that the user should be a member of.
	OrganizationIDs []uuid.UUID `json:"organization_ids" validate:"" format:"uuid"`
	// Service accounts are admin-managed accounts that cannot login.
	ServiceAccount bool `json:"service_account,omitempty"`
	// Roles is an optional list of site-level roles to assign at creation.
	Roles []string `json:"roles,omitempty"`
}

// CreateUserRequestWithOrgs is kept for callers that predate the rename.
// Deprecated: Use CreateUserRequest instead.
// @typescript-ignore CreateUserRequestWithOrgs
type CreateUserRequestWithOrgs = CreateUserRequest

type UpdateUserProfileRequest struct {
	Username string `json:"username" validate:"required,username"`
	Name     string `json:"name" validate:"user_real_name"`
	// AvatarURL is only applied for users whose login type is password or
	// none. For other login types the avatar is synced from the identity
	// provider on login, so a submitted value is ignored.
	AvatarURL string `json:"avatar_url" format:"uri"`
}

// UpdateUserEmailRequest changes a user's email by matching their current
// email address. This API is experimental and may change without notice.
type UpdateUserEmailRequest struct {
	OldEmail string `json:"old_email" validate:"required,email" format:"email"`
	NewEmail string `json:"new_email" validate:"required,email" format:"email"`
}

type ValidateUserPasswordRequest struct {
	Password string `json:"password" validate:"required"`
}

type ValidateUserPasswordResponse struct {
	Valid   bool   `json:"valid"`
	Details string `json:"details"`
}

// TerminalFontName is the name of supported terminal font
type TerminalFontName string

var TerminalFontNames = []TerminalFontName{
	TerminalFontUnknown, TerminalFontGeistMono, TerminalFontIBMPlexMono,
	TerminalFontFiraCode, TerminalFontSourceCodePro, TerminalFontJetBrainsMono,
}

const (
	TerminalFontUnknown       TerminalFontName = ""
	TerminalFontGeistMono     TerminalFontName = "geist-mono"
	TerminalFontIBMPlexMono   TerminalFontName = "ibm-plex-mono"
	TerminalFontFiraCode      TerminalFontName = "fira-code"
	TerminalFontSourceCodePro TerminalFontName = "source-code-pro"
	TerminalFontJetBrainsMono TerminalFontName = "jetbrains-mono"
)

type ThemeMode string

const (
	// ThemeModeUnset is the server-side default when the user has never
	// set a theme_mode. It is also stored for legacy auto preferences so
	// clients can migrate the old sync-with-system setting. Clients should
	// inspect ThemePreference for legacy auto values before treating unset
	// mode as ThemeModeSingle for backward compatibility with PR #24672.
	ThemeModeUnset  ThemeMode = ""
	ThemeModeSync   ThemeMode = "sync"
	ThemeModeSingle ThemeMode = "single"
)

type UserAppearanceSettings struct {
	// ThemePreference is the legacy single-field appearance setting. In
	// "single" mode it mirrors the active theme. In "sync" mode modern
	// clients normally mirror the active OS slot, but older clients can
	// update only this field, so it may diverge from ThemeLight or
	// ThemeDark until a modern client saves the full appearance state
	// again.
	ThemePreference string    `json:"theme_preference"`
	ThemeMode       ThemeMode `json:"theme_mode"`
	// Ignored when ThemeMode is "single"
	ThemeLight string `json:"theme_light"`
	// Ignored when ThemeMode is "single"
	ThemeDark    string           `json:"theme_dark"`
	TerminalFont TerminalFontName `json:"terminal_font"`
}

type UpdateUserAppearanceSettingsRequest struct {
	ThemePreference string `json:"theme_preference" validate:"required"`
	// ThemeMode is optional for backward compatibility. When empty,
	// the server leaves theme_mode, theme_light, and theme_dark
	// unchanged so older CLI clients do not erase sync-mode settings.
	// Legacy auto preferences are the exception: they clear theme_mode
	// so clients can migrate the old sync-with-system setting.
	ThemeMode ThemeMode `json:"theme_mode" validate:"omitempty,oneof=sync single"`
	// ThemeLight is required when ThemeMode is "sync". In "single"
	// mode an empty value means "preserve the previously persisted
	// slot" rather than "clear the slot", so partial updates that send
	// only one slot keep the other intact.
	ThemeLight string `json:"theme_light" validate:"required_if=ThemeMode sync,omitempty,oneof=light light-protan-deuter light-tritan dark dark-protan-deuter dark-tritan"`
	// ThemeDark is required when ThemeMode is "sync". In "single" mode
	// an empty value means "preserve the previously persisted slot"
	// rather than "clear the slot", so partial updates that send only
	// one slot keep the other intact.
	ThemeDark    string           `json:"theme_dark" validate:"required_if=ThemeMode sync,omitempty,oneof=light light-protan-deuter light-tritan dark dark-protan-deuter dark-tritan"`
	TerminalFont TerminalFontName `json:"terminal_font" validate:"required"`
}

type UserPreferenceSettings struct {
	ThinkingDisplayMode    ThinkingDisplayMode   `json:"thinking_display_mode"`
	ShellToolDisplayMode   AgentDisplayMode      `json:"shell_tool_display_mode"`
	CodeDiffDisplayMode    AgentDisplayMode      `json:"code_diff_display_mode"`
	CollapseAssistantSteps bool                  `json:"collapse_assistant_steps"`
	AgentChatSendShortcut  AgentChatSendShortcut `json:"agent_chat_send_shortcut"`
}

type UpdateUserPreferenceSettingsRequest struct {
	ThinkingDisplayMode    ThinkingDisplayMode   `json:"thinking_display_mode,omitempty"`
	ShellToolDisplayMode   AgentDisplayMode      `json:"shell_tool_display_mode,omitempty"`
	CodeDiffDisplayMode    AgentDisplayMode      `json:"code_diff_display_mode,omitempty"`
	CollapseAssistantSteps *bool                 `json:"collapse_assistant_steps,omitempty"`
	AgentChatSendShortcut  AgentChatSendShortcut `json:"agent_chat_send_shortcut,omitempty"`
}

type AgentChatSendShortcut string

const (
	AgentChatSendShortcutEnter         AgentChatSendShortcut = "enter"
	AgentChatSendShortcutModifierEnter AgentChatSendShortcut = "modifier_enter"
)

var ValidAgentChatSendShortcuts = []AgentChatSendShortcut{
	AgentChatSendShortcutEnter,
	AgentChatSendShortcutModifierEnter,
}

type ThinkingDisplayMode string

const (
	ThinkingDisplayModeAuto            ThinkingDisplayMode = "auto"
	ThinkingDisplayModePreview         ThinkingDisplayMode = "preview"
	ThinkingDisplayModeAlwaysExpanded  ThinkingDisplayMode = "always_expanded"
	ThinkingDisplayModeAlwaysCollapsed ThinkingDisplayMode = "always_collapsed"
)

var ValidThinkingDisplayModes = []ThinkingDisplayMode{
	ThinkingDisplayModeAuto,
	ThinkingDisplayModePreview,
	ThinkingDisplayModeAlwaysExpanded,
	ThinkingDisplayModeAlwaysCollapsed,
}

type AgentDisplayMode string

const (
	AgentDisplayModeAuto            AgentDisplayMode = "auto"
	AgentDisplayModeAlwaysExpanded  AgentDisplayMode = "always_expanded"
	AgentDisplayModeAlwaysCollapsed AgentDisplayMode = "always_collapsed"
)

var ValidAgentDisplayModes = []AgentDisplayMode{
	AgentDisplayModeAuto,
	AgentDisplayModeAlwaysExpanded,
	AgentDisplayModeAlwaysCollapsed,
}

type UpdateUserPasswordRequest struct {
	OldPassword string `json:"old_password" validate:""`
	Password    string `json:"password" validate:"required"`
}

type UserQuietHoursScheduleResponse struct {
	RawSchedule string `json:"raw_schedule"`
	// UserSet is true if the user has set their own quiet hours schedule. If
	// false, the user is using the default schedule.
	UserSet bool `json:"user_set"`
	// UserCanSet is true if the user is allowed to set their own quiet hours
	// schedule. If false, the user cannot set a custom schedule and the default
	// schedule will always be used.
	UserCanSet bool `json:"user_can_set"`
	// Time is the time of day that the quiet hours window starts in the given
	// Timezone each day.
	Time     string `json:"time"`     // HH:mm (24-hour)
	Timezone string `json:"timezone"` // raw format from the cron expression, UTC if unspecified
	// Next is the next time that the quiet hours window will start.
	Next time.Time `json:"next" format:"date-time"`
}

type UpdateUserQuietHoursScheduleRequest struct {
	// Schedule is a cron expression that defines when the user's quiet hours
	// window is. Schedule must not be empty. For new users, the schedule is set
	// to 2am in their browser or computer's timezone. The schedule denotes the
	// beginning of a 4 hour window where the workspace is allowed to
	// automatically stop or restart due to maintenance or template schedule.
	//
	// The schedule must be daily with a single time, and should have a timezone
	// specified via a CRON_TZ prefix (otherwise UTC will be used).
	//
	// If the schedule is empty, the user will be updated to use the default
	// schedule.
	Schedule string `json:"schedule" validate:"required"`
}

type UpdateRoles struct {
	Roles []string `json:"roles" validate:""`
}

type UserRoles struct {
	Roles             []string               `json:"roles"`
	OrganizationRoles map[uuid.UUID][]string `json:"organization_roles"`
}

type ConvertLoginRequest struct {
	// ToType is the login type to convert to.
	ToType   LoginType `json:"to_type" validate:"required"`
	Password string    `json:"password" validate:"required"`
}

// LoginWithPasswordRequest enables callers to authenticate with email and password.
type LoginWithPasswordRequest struct {
	Email    string `json:"email" validate:"required,email" format:"email"`
	Password string `json:"password" validate:"required"`
}

// LoginWithPasswordResponse contains a session token for the newly authenticated user.
type LoginWithPasswordResponse struct {
	SessionToken string `json:"session_token" validate:"required"`
}

// RequestOneTimePasscodeRequest enables callers to request a one-time-passcode to change their password.
type RequestOneTimePasscodeRequest struct {
	Email string `json:"email" validate:"required,email" format:"email"`
}

// ChangePasswordWithOneTimePasscodeRequest enables callers to change their password when they've forgotten it.
type ChangePasswordWithOneTimePasscodeRequest struct {
	Email           string `json:"email" validate:"required,email" format:"email"`
	Password        string `json:"password" validate:"required"`
	OneTimePasscode string `json:"one_time_passcode" validate:"required"`
}

type OAuthConversionResponse struct {
	StateString string    `json:"state_string"`
	ExpiresAt   time.Time `json:"expires_at" format:"date-time"`
	ToType      LoginType `json:"to_type"`
	UserID      uuid.UUID `json:"user_id" format:"uuid"`
}

// AuthMethods contains authentication method information like whether they are enabled or not or custom text, etc.
type AuthMethods struct {
	TermsOfServiceURL string           `json:"terms_of_service_url,omitempty"`
	Password          AuthMethod       `json:"password"`
	Github            GithubAuthMethod `json:"github"`
	OIDC              OIDCAuthMethod   `json:"oidc"`
}

type AuthMethod struct {
	Enabled bool `json:"enabled"`
}

type UserLoginType struct {
	LoginType LoginType `json:"login_type"`
}

type GithubAuthMethod struct {
	Enabled                   bool `json:"enabled"`
	DefaultProviderConfigured bool `json:"default_provider_configured"`
}

type OIDCAuthMethod struct {
	AuthMethod
	SignInText string `json:"signInText"`
	IconURL    string `json:"iconUrl"`
}

// OIDCClaimsResponse represents the merged OIDC claims for a user.
type OIDCClaimsResponse struct {
	// Claims are the merged claims from the OIDC provider. These
	// are the union of the ID token claims and the userinfo claims,
	// where userinfo claims take precedence on conflict.
	Claims map[string]interface{} `json:"claims"`
}

// HasFirstUser returns whether the first user has been created.
func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
	res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/first", nil)
	if err != nil {
		return false, err
	}
	defer res.Body.Close()

	if res.StatusCode == http.StatusNotFound {
		// ensure we are talking to coder and not
		// some other service that returns 404
		v := res.Header.Get(BuildVersionHeader)
		if v == "" {
			return false, xerrors.Errorf("missing build version header, not a coder instance")
		}

		return false, nil
	}
	if res.StatusCode != http.StatusOK {
		return false, ReadBodyAsError(res)
	}
	return true, nil
}

// CreateFirstUser attempts to create the first user on a Coder deployment.
// This initial user has superadmin privileges. If >0 users exist, this request will fail.
func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest) (CreateFirstUserResponse, error) {
	res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/first", req)
	if err != nil {
		return CreateFirstUserResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusCreated {
		return CreateFirstUserResponse{}, ReadBodyAsError(res)
	}
	var resp CreateFirstUserResponse
	return resp, ReadBodyAsJSON(res, &resp)
}

// CreateUser creates a new user.
func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, error) {
	res, err := c.Request(ctx, http.MethodPost, "/api/v2/users", req)
	if err != nil {
		return User{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusCreated {
		return User{}, ReadBodyAsError(res)
	}
	var user User
	return user, ReadBodyAsJSON(res, &user)
}

// CreateUserWithOrgs creates a new user.
// Deprecated: Use CreateUser instead.
func (c *Client) CreateUserWithOrgs(ctx context.Context, req CreateUserRequest) (User, error) {
	return c.CreateUser(ctx, req)
}

// DeleteUser deletes a user.
func (c *Client) DeleteUser(ctx context.Context, id uuid.UUID) error {
	res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/users/%s", id), nil)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	// Check for a 200 or a 204 response. 2.14.0 accidentally included a 204 response,
	// which was a breaking change, and reverted in 2.14.1.
	if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
		return ReadBodyAsError(res)
	}
	return nil
}

// UpdateUserProfile updates the username of a user.
func (c *Client) UpdateUserProfile(ctx context.Context, user string, req UpdateUserProfileRequest) (User, error) {
	res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", user), req)
	if err != nil {
		return User{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return User{}, ReadBodyAsError(res)
	}
	var resp User
	return resp, ReadBodyAsJSON(res, &resp)
}

// UpdateUserEmail changes a user's email address. This API is experimental and
// may change without notice.
func (c *Client) UpdateUserEmail(ctx context.Context, req UpdateUserEmailRequest) error {
	res, err := c.Request(ctx, http.MethodPut, "/api/experimental/users/email", req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusNoContent {
		return ReadBodyAsError(res)
	}
	return nil
}

// ValidateUserPassword validates the complexity of a user password and that it is secured enough.
func (c *Client) ValidateUserPassword(ctx context.Context, req ValidateUserPasswordRequest) (ValidateUserPasswordResponse, error) {
	res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/validate-password", req)
	if err != nil {
		return ValidateUserPasswordResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return ValidateUserPasswordResponse{}, ReadBodyAsError(res)
	}
	var resp ValidateUserPasswordResponse
	return resp, ReadBodyAsJSON(res, &resp)
}

// UpdateUserStatus sets the user status to the given status
func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserStatus) (User, error) {
	path := fmt.Sprintf("/api/v2/users/%s/status/", user)
	switch status {
	case UserStatusActive:
		path += "activate"
	case UserStatusSuspended:
		path += "suspend"
	default:
		return User{}, xerrors.Errorf("status %q is not supported", status)
	}

	res, err := c.Request(ctx, http.MethodPut, path, nil)
	if err != nil {
		return User{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return User{}, ReadBodyAsError(res)
	}

	var resp User
	return resp, ReadBodyAsJSON(res, &resp)
}

// GetUserAppearanceSettings fetches the appearance settings for a user.
func (c *Client) GetUserAppearanceSettings(ctx context.Context, user string) (UserAppearanceSettings, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/appearance", user), nil)
	if err != nil {
		return UserAppearanceSettings{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return UserAppearanceSettings{}, ReadBodyAsError(res)
	}
	var resp UserAppearanceSettings
	return resp, ReadBodyAsJSON(res, &resp)
}

// UpdateUserAppearanceSettings updates the appearance settings for a user.
func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, req UpdateUserAppearanceSettingsRequest) (UserAppearanceSettings, error) {
	res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/appearance", user), req)
	if err != nil {
		return UserAppearanceSettings{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return UserAppearanceSettings{}, ReadBodyAsError(res)
	}
	var resp UserAppearanceSettings
	return resp, ReadBodyAsJSON(res, &resp)
}

// GetUserPreferenceSettings fetches the preference settings for a user.
func (c *Client) GetUserPreferenceSettings(ctx context.Context, user string) (UserPreferenceSettings, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/preferences", user), nil)
	if err != nil {
		return UserPreferenceSettings{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return UserPreferenceSettings{}, ReadBodyAsError(res)
	}
	var resp UserPreferenceSettings
	return resp, ReadBodyAsJSON(res, &resp)
}

// UpdateUserPreferenceSettings updates the preference settings for a user.
func (c *Client) UpdateUserPreferenceSettings(ctx context.Context, user string, req UpdateUserPreferenceSettingsRequest) (UserPreferenceSettings, error) {
	res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/preferences", user), req)
	if err != nil {
		return UserPreferenceSettings{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return UserPreferenceSettings{}, ReadBodyAsError(res)
	}
	var resp UserPreferenceSettings
	return resp, ReadBodyAsJSON(res, &resp)
}

// UpdateUserPassword updates a user password.
// It calls PUT /users/{user}/password
func (c *Client) UpdateUserPassword(ctx context.Context, user string, req UpdateUserPasswordRequest) error {
	res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", user), req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusNoContent {
		return ReadBodyAsError(res)
	}
	return nil
}

// PostOrganizationMember adds a user to an organization
func (c *Client) PostOrganizationMember(ctx context.Context, organizationID uuid.UUID, user string) (OrganizationMember, error) {
	res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/members/%s", organizationID, user), nil)
	if err != nil {
		return OrganizationMember{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return OrganizationMember{}, ReadBodyAsError(res)
	}
	var member OrganizationMember
	return member, ReadBodyAsJSON(res, &member)
}

// DeleteOrganizationMember removes a user from an organization
func (c *Client) DeleteOrganizationMember(ctx context.Context, organizationID uuid.UUID, user string) error {
	res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/organizations/%s/members/%s", organizationID, user), nil)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusNoContent {
		return ReadBodyAsError(res)
	}
	return nil
}

type OrganizationMembersQuery struct {
	UserID        uuid.UUID
	IncludeSystem bool
	GithubUserID  int64
}

func (omq OrganizationMembersQuery) AsRequestOption() RequestOption {
	return func(r *http.Request) {
		q := r.URL.Query()
		var sb strings.Builder
		if omq.UserID != uuid.Nil {
			_, _ = sb.WriteString("user_id:")
			_, _ = sb.WriteString(omq.UserID.String())
			_, _ = sb.WriteString(" ")
		}
		if omq.IncludeSystem {
			_, _ = sb.WriteString("include_system:true")
		}
		if omq.GithubUserID != 0 {
			_, _ = sb.WriteString("github_user_id:")
			_, _ = sb.WriteString(strconv.FormatInt(omq.GithubUserID, 10))
			_, _ = sb.WriteString(" ")
		}
		qs := strings.TrimSpace(sb.String())
		if len(qs) == 0 {
			return
		}
		q.Set("q", qs)
		r.URL.RawQuery = q.Encode()
	}
}

type OrganizationMembersQueryOption func(*OrganizationMembersQuery)

func OrganizationMembersQueryOptionUserID(userID uuid.UUID) OrganizationMembersQueryOption {
	return func(query *OrganizationMembersQuery) {
		query.UserID = userID
	}
}

func OrganizationMembersQueryOptionIncludeSystem() OrganizationMembersQueryOption {
	return func(query *OrganizationMembersQuery) {
		query.IncludeSystem = true
	}
}

func OrganizationMembersQueryOptionGithubUserID(githubUserID int64) OrganizationMembersQueryOption {
	return func(query *OrganizationMembersQuery) {
		query.GithubUserID = githubUserID
	}
}

func (c *Client) OrganizationMember(ctx context.Context, organizationIdent, userIdent string) (OrganizationMemberWithUserData, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/%s", organizationIdent, userIdent), nil)
	if err != nil {
		return OrganizationMemberWithUserData{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return OrganizationMemberWithUserData{}, ReadBodyAsError(res)
	}
	var member OrganizationMemberWithUserData
	return member, ReadBodyAsJSON(res, &member)
}

// OrganizationMembers lists all members in an organization
func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UUID, opts ...OrganizationMembersQueryOption) ([]OrganizationMemberWithUserData, error) {
	var query OrganizationMembersQuery
	for _, opt := range opts {
		opt(&query)
	}
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/", organizationID), nil, query.AsRequestOption())
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return nil, ReadBodyAsError(res)
	}
	var members []OrganizationMemberWithUserData
	return members, ReadBodyAsJSON(res, &members)
}

// OrganizationMembers lists filtered and paginated members in an organization
func (c *Client) OrganizationMembersPaginated(ctx context.Context, organizationID uuid.UUID, req UsersRequest) (PaginatedMembersResponse, error) {
	res, err := c.Request(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/organizations/%s/paginated-members", organizationID),
		nil,
		req.Pagination.asRequestOption(),
		req.asRequestOption(),
	)
	if err != nil {
		return PaginatedMembersResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return PaginatedMembersResponse{}, ReadBodyAsError(res)
	}
	var membersRes PaginatedMembersResponse
	return membersRes, ReadBodyAsJSON(res, &membersRes)
}

// UpdateUserRoles grants the userID the specified roles.
// Include ALL roles the user has.
func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRoles) (User, error) {
	res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/roles", user), req)
	if err != nil {
		return User{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return User{}, ReadBodyAsError(res)
	}
	var resp User
	return resp, ReadBodyAsJSON(res, &resp)
}

// UpdateOrganizationMemberRoles grants the userID the specified roles in an org.
// Include ALL roles the user has.
func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organizationID uuid.UUID, user string, req UpdateRoles) (OrganizationMember, error) {
	res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/organizations/%s/members/%s/roles", organizationID, user), req)
	if err != nil {
		return OrganizationMember{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return OrganizationMember{}, ReadBodyAsError(res)
	}
	var member OrganizationMember
	return member, ReadBodyAsJSON(res, &member)
}

// UserRoles returns all roles the user has
func (c *Client) UserRoles(ctx context.Context, user string) (UserRoles, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", user), nil)
	if err != nil {
		return UserRoles{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return UserRoles{}, ReadBodyAsError(res)
	}
	var roles UserRoles
	return roles, ReadBodyAsJSON(res, &roles)
}

// UserOIDCClaims returns the merged OIDC claims for the authenticated user.
func (c *Client) UserOIDCClaims(ctx context.Context) (OIDCClaimsResponse, error) {
	res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/oidc-claims", nil)
	if err != nil {
		return OIDCClaimsResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return OIDCClaimsResponse{}, ReadBodyAsError(res)
	}
	var resp OIDCClaimsResponse
	return resp, ReadBodyAsJSON(res, &resp)
}

// LoginWithPassword creates a session token authenticating with an email and password.
// Call `SetSessionToken()` to apply the newly acquired token to the client.
func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordRequest) (LoginWithPasswordResponse, error) {
	res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/login", req)
	if err != nil {
		return LoginWithPasswordResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusCreated {
		return LoginWithPasswordResponse{}, ReadBodyAsError(res)
	}
	var resp LoginWithPasswordResponse
	err = ReadBodyAsJSON(res, &resp)
	if err != nil {
		return LoginWithPasswordResponse{}, err
	}
	return resp, nil
}

func (c *Client) RequestOneTimePasscode(ctx context.Context, req RequestOneTimePasscodeRequest) error {
	res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/otp/request", req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusNoContent {
		return ReadBodyAsError(res)
	}

	return nil
}

func (c *Client) ChangePasswordWithOneTimePasscode(ctx context.Context, req ChangePasswordWithOneTimePasscodeRequest) error {
	res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/otp/change-password", req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusNoContent {
		return ReadBodyAsError(res)
	}

	return nil
}

// ConvertLoginType will send a request to convert the user from password
// based authentication to oauth based. The response has the oauth state code
// to use in the oauth flow.
func (c *Client) ConvertLoginType(ctx context.Context, req ConvertLoginRequest) (OAuthConversionResponse, error) {
	return c.ConvertUserLoginType(ctx, Me, req)
}

// ConvertUserLoginType will send a request to convert the user from password
// based authentication to oauth based. The response has the oauth state code
// to use in the oauth flow.
func (c *Client) ConvertUserLoginType(ctx context.Context, user string, req ConvertLoginRequest) (OAuthConversionResponse, error) {
	res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/convert-login", user), req)
	if err != nil {
		return OAuthConversionResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusCreated {
		return OAuthConversionResponse{}, ReadBodyAsError(res)
	}
	var resp OAuthConversionResponse
	err = ReadBodyAsJSON(res, &resp)
	if err != nil {
		return OAuthConversionResponse{}, err
	}
	return resp, nil
}

// Logout calls the /logout API
// Call `ClearSessionToken()` to clear the session token of the client.
func (c *Client) Logout(ctx context.Context) error {
	// Since `LoginWithPassword` doesn't actually set a SessionToken
	// (it requires a call to SetSessionToken), this is essentially a no-op
	res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/logout", nil)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	return nil
}

// User returns a user for the ID/username provided.
func (c *Client) User(ctx context.Context, userIdent string) (User, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", userIdent), nil)
	if err != nil {
		return User{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return User{}, ReadBodyAsError(res)
	}
	var user User
	return user, ReadBodyAsJSON(res, &user)
}

// UserQuietHoursSchedule returns the quiet hours settings for the user. This
// endpoint only exists in enterprise editions.
func (c *Client) UserQuietHoursSchedule(ctx context.Context, userIdent string) (UserQuietHoursScheduleResponse, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/quiet-hours", userIdent), nil)
	if err != nil {
		return UserQuietHoursScheduleResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return UserQuietHoursScheduleResponse{}, ReadBodyAsError(res)
	}
	var resp UserQuietHoursScheduleResponse
	return resp, ReadBodyAsJSON(res, &resp)
}

// UpdateUserQuietHoursSchedule updates the quiet hours settings for the user.
// This endpoint only exists in enterprise editions.
func (c *Client) UpdateUserQuietHoursSchedule(ctx context.Context, userIdent string, req UpdateUserQuietHoursScheduleRequest) (UserQuietHoursScheduleResponse, error) {
	res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/quiet-hours", userIdent), req)
	if err != nil {
		return UserQuietHoursScheduleResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return UserQuietHoursScheduleResponse{}, ReadBodyAsError(res)
	}
	var resp UserQuietHoursScheduleResponse
	return resp, ReadBodyAsJSON(res, &resp)
}

// Users returns all users according to the request parameters. If no parameters are set,
// the default behavior is to return all users in a single page.
func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, error) {
	res, err := c.Request(ctx, http.MethodGet, "/api/v2/users", nil,
		req.Pagination.asRequestOption(),
		req.asRequestOption(),
	)
	if err != nil {
		return GetUsersResponse{}, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return GetUsersResponse{}, ReadBodyAsError(res)
	}

	var usersRes GetUsersResponse
	return usersRes, ReadBodyAsJSON(res, &usersRes)
}

// OrganizationsByUser returns all organizations the user is a member of.
func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organization, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", user), nil)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode > http.StatusOK {
		return nil, ReadBodyAsError(res)
	}
	var orgs []Organization
	return orgs, ReadBodyAsJSON(res, &orgs)
}

func (c *Client) OrganizationByUserAndName(ctx context.Context, user string, name string) (Organization, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil)
	if err != nil {
		return Organization{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return Organization{}, ReadBodyAsError(res)
	}
	var org Organization
	return org, ReadBodyAsJSON(res, &org)
}

// AuthMethods returns types of authentication available to the user.
func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) {
	res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/authmethods", nil)
	if err != nil {
		return AuthMethods{}, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return AuthMethods{}, ReadBodyAsError(res)
	}

	var userAuth AuthMethods
	return userAuth, ReadBodyAsJSON(res, &userAuth)
}