// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package services import ( "context" "fmt" "net/http" "net/url" "strconv" gitea "gitea.dev/sdk" "gitea.dev/tea/internal/transport" ) // AdminUser is the explicit account-management result, without authentication secrets. type AdminUser struct { ID int64 `json:"id"` Username string `json:"username"` Email string `json:"email"` FullName string `json:"full_name"` Description string `json:"description"` Website string `json:"website"` Location string `json:"location"` URL string `json:"url"` Admin bool `json:"admin"` Active bool `json:"active"` Restricted bool `json:"restricted"` ProhibitLogin bool `json:"prohibit_login"` Visibility string `json:"visibility"` CreatedAt string `json:"created_at"` } func adminUserFrom(user *gitea.User) AdminUser { return AdminUser{ ID: user.ID, Username: user.UserName, Email: user.Email, FullName: user.FullName, Description: user.Description, Website: user.Website, Location: user.Location, URL: user.HTMLURL, Admin: user.IsAdmin, Active: user.IsActive, Restricted: user.Restricted, ProhibitLogin: user.ProhibitLogin, Visibility: string(user.Visibility), CreatedAt: timestamp(user.Created), } } // AdminUserOptions distinguishes omitted fields from explicit empty or false values. type AdminUserOptions struct { Email *string `json:"email,omitempty"` FullName *string `json:"full_name,omitempty"` Description *string `json:"description,omitempty"` Website *string `json:"website,omitempty"` Location *string `json:"location,omitempty"` Visibility *string `json:"visibility,omitempty"` Password string `json:"password,omitempty"` Admin *bool `json:"admin,omitempty"` Active *bool `json:"active,omitempty"` Restricted *bool `json:"restricted,omitempty"` ProhibitLogin *bool `json:"prohibit_login,omitempty"` MustChangePassword *bool `json:"must_change_password,omitempty"` AllowGitHook *bool `json:"allow_git_hook,omitempty"` AllowImportLocal *bool `json:"allow_import_local,omitempty"` AllowCreateOrganization *bool `json:"allow_create_organization,omitempty"` MaxRepoCreation *int `json:"max_repo_creation,omitempty"` } func visibilityOf(visibility *string) *gitea.VisibleType { if visibility == nil { return nil } value := gitea.VisibleType(*visibility) return &value } // AdminUsers provides administrator account operations. type AdminUsers struct{ Client *transport.Client } // List returns a page of the users whose username, full name or email contains query, or of // every user without one. func (s AdminUsers) List(ctx context.Context, query string, limit, page int) (Page[AdminUser], error) { users, err := callPage(ctx, s.Client, limit, page, func(ctx context.Context, c *gitea.Client, list gitea.ListOptions) ([]*gitea.User, *gitea.Response, error) { return c.Admin.ListUsers(ctx, gitea.AdminListUsersOptions{ListOptions: list, Query: query}) }) return convertPage(users, adminUserFrom), err } // Search finds accounts whose name contains query; unlike List it needs no administrator rights. func (s AdminUsers) Search(ctx context.Context, query string, limit int) ([]AdminUser, error) { users, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.User, *gitea.Response, error) { return c.Users.SearchUsers(ctx, gitea.SearchUsersOption{ListOptions: listOptions(limit, 1), KeyWord: query}) }) return convertAll(users, adminUserFrom), err } func (s AdminUsers) get(ctx context.Context, username string) (*gitea.User, error) { user, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.User, *gitea.Response, error) { return c.Users.GetUserInfo(ctx, username) }) return user, err } // View retrieves one account. func (s AdminUsers) View(ctx context.Context, username string) (AdminUser, error) { user, err := s.get(ctx, username) if err != nil { return AdminUser{}, err } return adminUserFrom(user), nil } // Create creates an account, then applies administrative fields unavailable during creation. func (s AdminUsers) Create(ctx context.Context, username string, opts AdminUserOptions) (AdminUser, error) { created, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.User, *gitea.Response, error) { return c.Admin.CreateUser(ctx, gitea.CreateUserOption{ Username: username, LoginName: username, Email: deref(opts.Email), FullName: deref(opts.FullName), Password: opts.Password, MustChangePassword: opts.MustChangePassword, Visibility: visibilityOf(opts.Visibility), }) }) if err != nil { return AdminUser{}, err } if opts.Admin == nil && opts.Restricted == nil && opts.ProhibitLogin == nil { return adminUserFrom(created), nil } followup := AdminUserOptions{Admin: opts.Admin, Restricted: opts.Restricted, ProhibitLogin: opts.ProhibitLogin} if err := s.edit(ctx, username, created, followup); err != nil { return adminUserFrom(created), fmt.Errorf("user %s created but account restrictions could not be applied: %w", username, err) } item, err := s.View(ctx, username) if err != nil { return adminUserFrom(created), fmt.Errorf("user %s created but could not be reloaded: %w", username, err) } return item, nil } func (s AdminUsers) edit(ctx context.Context, username string, current *gitea.User, opts AdminUserOptions) error { return do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) { return c.Admin.EditUser(ctx, username, gitea.EditUserOption{ SourceID: current.SourceID, LoginName: current.LoginName, Email: opts.Email, FullName: opts.FullName, Password: opts.Password, Description: opts.Description, MustChangePassword: opts.MustChangePassword, Website: opts.Website, Location: opts.Location, Active: opts.Active, Admin: opts.Admin, AllowGitHook: opts.AllowGitHook, AllowImportLocal: opts.AllowImportLocal, MaxRepoCreation: opts.MaxRepoCreation, ProhibitLogin: opts.ProhibitLogin, AllowCreateOrganization: opts.AllowCreateOrganization, Restricted: opts.Restricted, Visibility: visibilityOf(opts.Visibility), }) }) } // Edit preserves the authentication identity while updating account properties. func (s AdminUsers) Edit(ctx context.Context, username string, opts AdminUserOptions) (AdminUser, error) { current, err := s.get(ctx, username) if err != nil { return AdminUser{}, err } if err := s.edit(ctx, username, current, opts); err != nil { return AdminUser{}, err } return s.View(ctx, username) } // Delete removes an account. Gitea refuses to delete users who own repositories or organizations // unless purge also deletes what they own. func (s AdminUsers) Delete(ctx context.Context, username string, purge bool) error { var err error if purge { // TODO(sdk): DeleteUser needs a purge option. err = sendJSON(ctx, s.Client, http.MethodDelete, "admin/users/"+url.PathEscape(username)+"?purge=true", nil, nil) } else { err = do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) { return c.Admin.DeleteUser(ctx, username) }) } return Classify(err, "user", strconv.Quote(username), "") }