mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
- **switch tea to the Cobra command tree:** `internal/app` wires the packages from parts 1–4 into explicit noun/verb commands with `--repo`, `--json` / `--jq`, `--sudo` and `--debug`; `main.go` points at it. Build, release and docs tooling (`tools/cli-docs`, Makefile, goreleaser, snap, CI) and README / CONTRIBUTING are updated. - **remove the urfave/cli command tree:** deletes `cmd/`, `modules/`, the old integration tests, contrib completion scripts and the generated `docs/CLI.md`, and drops their dependencies. This commit only deletes. ## Breaking changes This is a breaking release. It requires Gitea 28 or newer; configuration moves to separate `config.yml` and `hosts.yml` files and requires fresh authentication, and command names, aliases, flags and output formats change. `docs/migration-1.0.md` maps every old command, flag and environment variable to its replacement. ## Issues fixes #1118 fixes #1116 fixes #1115 fixes #1094 fixes #1091 fixes #1073 fixes #1052 fixes #1032 fixes #1023 fixes #966 fixes #908 fixes #833 fixes #831 fixes #817 fixes #804 fixes #779 fixes #770 fixes #737 fixes #697 fixes #726 fixes #677 fixes #672 fixes #633 fixes #632 fixes #560 fixes #556 fixes #523 fixes #516 fixes #510 fixes #504 fixes #500 fixes #482 fixes #406 fixes #403 fixes #373 fixes #351 fixes #344 fixes #326 fixes #325 fixes #323 fixes #246 fixes #240 fixes #232 fixes #197 fixes #172 fixes #141 fixes #69 fixes #1110 fixes #569 fixes #480 fixes #666 fixes #670 fixes #457 fixes #643 fixes #498 fixes #490 fixes #1069 fixes #834 fixes #602 fixes #328 fixes #417 fixes #571 fixes #1029 fixes #489 fixes #1050 fixes #162 fixes #430 fixes #1087 fixes #683 Reviewed-on: https://gitea.com/gitea/tea/pulls/1126 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
211 lines
8.3 KiB
Go
211 lines
8.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"net/mail"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.dev/tea/internal/clierr"
|
|
"gitea.dev/tea/internal/services"
|
|
"gitea.dev/tea/internal/terminal"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func (a *application) adminCommand() *cobra.Command {
|
|
group := &cobra.Command{Use: "admin", Short: "Manage instance administration"}
|
|
users := &cobra.Command{Use: "user", Short: "Manage registered accounts"}
|
|
group.AddCommand(users)
|
|
var limit, page int
|
|
list := &cobra.Command{Use: "list", Short: "List registered accounts", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
|
target, err := a.resolve(cmd.Context(), false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items, err := (services.AdminUsers{Client: target.client}).List(cmd.Context(), limit, page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeList(a, cmd, items, adminUserRow)
|
|
}}
|
|
pageFlags(list, &limit, &page)
|
|
users.AddCommand(list)
|
|
for _, verb := range []string{"view", "create", "edit", "delete"} {
|
|
var username, email, fullName, description, website, location, visibility, password, passwordFile string
|
|
var admin, active, restricted, prohibitLogin, mustChangePassword, allowGitHook, allowImportLocal, allowCreateOrganization bool
|
|
var maxRepos int
|
|
opts := services.AdminUserOptions{}
|
|
command := &cobra.Command{Use: verb + " <username>", Short: titleCase(verb) + " an account", Args: argsExact(1)}
|
|
if verb == "create" {
|
|
command.Use = "create [username]"
|
|
command.Args = usageArgs(cobra.MaximumNArgs(1))
|
|
command.Flags().StringVar(&username, "username", "", "Account username")
|
|
}
|
|
if verb == "delete" {
|
|
command.Annotations = textOutput("account deletion does not return a resource")
|
|
}
|
|
type stringBinding struct {
|
|
name, description string
|
|
value *string
|
|
option **string
|
|
}
|
|
stringsToBind := []stringBinding{{"email", "Email address", &email, &opts.Email}, {"full-name", "Display name", &fullName, &opts.FullName}, {"visibility", "Profile visibility: public, limited, private", &visibility, &opts.Visibility}}
|
|
type boolBinding struct {
|
|
name, description string
|
|
value *bool
|
|
option **bool
|
|
}
|
|
boolsToBind := []boolBinding{{"admin", "Administrator privileges", &admin, &opts.Admin}, {"restricted", "Restrict account access", &restricted, &opts.Restricted}, {"prohibit-login", "Prohibit signing in", &prohibitLogin, &opts.ProhibitLogin}}
|
|
if verb == "edit" {
|
|
stringsToBind = append(stringsToBind, stringBinding{"description", "Profile description", &description, &opts.Description}, stringBinding{"website", "Website URL", &website, &opts.Website}, stringBinding{"location", "Location", &location, &opts.Location})
|
|
boolsToBind = append(boolsToBind, boolBinding{"active", "Activate account", &active, &opts.Active}, boolBinding{"allow-git-hook", "Allow Git hooks", &allowGitHook, &opts.AllowGitHook}, boolBinding{"allow-import-local", "Allow local repository imports", &allowImportLocal, &opts.AllowImportLocal}, boolBinding{"allow-create-organization", "Allow creating organizations", &allowCreateOrganization, &opts.AllowCreateOrganization})
|
|
command.Flags().IntVar(&maxRepos, "max-repo-creation", 0, "Repository creation limit (-1 for unlimited)")
|
|
}
|
|
if verb == "create" || verb == "edit" {
|
|
for _, binding := range stringsToBind {
|
|
command.Flags().StringVar(binding.value, binding.name, "", binding.description)
|
|
}
|
|
for _, binding := range boolsToBind {
|
|
command.Flags().BoolVar(binding.value, binding.name, false, binding.description+" (use =false to revoke)")
|
|
}
|
|
command.Flags().StringVar(&password, "password", "", "Password (prefer --password-file)")
|
|
command.Flags().StringVar(&passwordFile, "password-file", "", "Read password from file or - for stdin; remove one trailing line ending")
|
|
command.Flags().BoolVar(&mustChangePassword, "must-change-password", true, "Require password change on next sign-in (use =false to disable)")
|
|
}
|
|
command.RunE = func(cmd *cobra.Command, args []string) error {
|
|
prompted := false
|
|
if verb == "create" {
|
|
if len(args) > 0 {
|
|
if cmd.Flags().Changed("username") {
|
|
return clierr.Wrap(clierr.Usage, errors.New("supply username as an argument or --username, not both"))
|
|
}
|
|
username = args[0]
|
|
}
|
|
for _, field := range []struct {
|
|
value *string
|
|
flag string
|
|
}{{&username, "username"}, {&email, "email"}} {
|
|
completed, err := a.require(cmd, field.value, field.flag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
prompted = prompted || completed
|
|
}
|
|
opts.Email = &email
|
|
opts.FullName = &fullName
|
|
if visibility == "" {
|
|
visibility = "public"
|
|
}
|
|
opts.Visibility = &visibility
|
|
} else {
|
|
username = args[0]
|
|
}
|
|
if strings.TrimSpace(username) == "" || strings.ContainsAny(username, "/\\ \t\r\n") {
|
|
return clierr.Wrap(clierr.Usage, errors.New("invalid account username"))
|
|
}
|
|
if verb == "create" || verb == "edit" {
|
|
for _, binding := range stringsToBind {
|
|
if cmd.Flags().Changed(binding.name) {
|
|
*binding.option = binding.value
|
|
}
|
|
}
|
|
for _, binding := range boolsToBind {
|
|
if cmd.Flags().Changed(binding.name) {
|
|
*binding.option = binding.value
|
|
}
|
|
}
|
|
if opts.Email != nil {
|
|
address, err := mail.ParseAddress(email)
|
|
if err != nil || address.Address != email {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--email must contain one email address"))
|
|
}
|
|
}
|
|
if opts.Visibility != nil {
|
|
if err := oneOf("visibility", visibility, "public", "limited", "private"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if cmd.Flags().Changed("max-repo-creation") {
|
|
if maxRepos < -1 {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--max-repo-creation must be -1 or nonnegative"))
|
|
}
|
|
opts.MaxRepoCreation = &maxRepos
|
|
}
|
|
passwordChange := verb == "create" || cmd.Flags().Changed("password") || cmd.Flags().Changed("password-file")
|
|
if passwordChange {
|
|
var err error
|
|
if password, err = a.readSecret(cmd, password, passwordFile, "password", "password-file"); err != nil {
|
|
return err
|
|
}
|
|
if password == "" && !cmd.Flags().Changed("password-file") {
|
|
snapshot, err := a.deps.Settings.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !a.policy(snapshot).CanPrompt(a.deps.Streams) {
|
|
return clierr.Wrap(clierr.Usage, errors.New("missing password: supply --password-file or --password"))
|
|
}
|
|
password, err = a.deps.Prompter.SecretInput(cmd.Context(), "Password for "+username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
confirmation, err := a.deps.Prompter.SecretInput(cmd.Context(), "Confirm password")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if password != confirmation {
|
|
return clierr.Wrap(clierr.Usage, errors.New("passwords do not match"))
|
|
}
|
|
prompted = true
|
|
}
|
|
if password == "" {
|
|
return clierr.Wrap(clierr.Usage, errors.New("password cannot be empty"))
|
|
}
|
|
opts.Password = password
|
|
opts.MustChangePassword = &mustChangePassword
|
|
} else if cmd.Flags().Changed("must-change-password") {
|
|
opts.MustChangePassword = &mustChangePassword
|
|
}
|
|
|
|
if verb == "edit" && opts == (services.AdminUserOptions{}) {
|
|
return clierr.Wrap(clierr.Usage, errors.New("supply account fields to edit"))
|
|
}
|
|
}
|
|
target, err := a.resolve(cmd.Context(), false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if verb == "delete" || prompted {
|
|
if err := a.confirm(cmd, verb+" account "+username+" on "+target.Host); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
service := services.AdminUsers{Client: target.client}
|
|
var item services.AdminUser
|
|
switch verb {
|
|
case "view":
|
|
item, err = service.View(cmd.Context(), username)
|
|
case "create":
|
|
item, err = service.Create(cmd.Context(), username, opts)
|
|
case "edit":
|
|
item, err = service.Edit(cmd.Context(), username, opts)
|
|
case "delete":
|
|
return service.Delete(cmd.Context(), username)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeItem(a, cmd, item, adminUserRow)
|
|
}
|
|
users.AddCommand(command)
|
|
}
|
|
return group
|
|
}
|
|
|
|
func adminUserRow(item services.AdminUser) []terminal.Cell {
|
|
return terminal.Row(strconv.FormatInt(item.ID, 10), item.Username, item.FullName, "admin="+strconv.FormatBool(item.Admin), "active="+strconv.FormatBool(item.Active))
|
|
}
|