Files
gitea-tea/internal/services/admin_users.go
T

186 lines
7.9 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package services
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
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
}
// get finds an account by its exact username through the administrator listing. Unlike
// /users/{name}, that answers only administrators, only with complete role and status fields,
// and only for individual accounts, never for organizations.
func (s AdminUsers) get(ctx context.Context, username string) (*gitea.User, error) {
for page := 1; ; page++ {
users, err := callPage(ctx, s.Client, 50, 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: username})
})
if err != nil {
return nil, err
}
for _, user := range users.Items {
if strings.EqualFold(user.UserName, username) {
return user, nil
}
}
if !users.More {
return nil, NotFound("user", strconv.Quote(username), "")
}
}
}
// 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), "")
}