初始提交: Gitea 项目代码

This commit is contained in:
root
2026-05-30 22:47:36 +08:00
commit f288f76350
6116 changed files with 776822 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
// ListWorkflowJobs Lists all jobs
func ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/jobs admin listAdminWorkflowJobs
// ---
// summary: Lists all jobs
// produces:
// - application/json
// parameters:
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: sort
// in: query
// description: sort jobs by attribute. Supported values are "id". Default is "id"
// type: string
// - name: order
// in: query
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
// type: string
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.ListJobs(ctx, 0, 0, 0, nil)
}
// ListWorkflowRuns Lists all runs
func ListWorkflowRuns(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/runs admin listAdminWorkflowRuns
// ---
// summary: Lists all runs
// produces:
// - application/json
// parameters:
// - name: event
// in: query
// description: workflow event name
// type: string
// required: false
// - name: branch
// in: query
// description: workflow branch
// type: string
// required: false
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: actor
// in: query
// description: triggered by user
// type: string
// required: false
// - name: head_sha
// in: query
// description: triggering sha of the workflow run
// type: string
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WorkflowRunsList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, 0, 0)
}
+180
View File
@@ -0,0 +1,180 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
repo_service "gitea.dev/services/repository"
)
// ListUnadoptedRepositories lists the unadopted repositories that match the provided names
func ListUnadoptedRepositories(ctx *context.APIContext) {
// swagger:operation GET /admin/unadopted admin adminUnadoptedList
// ---
// summary: List unadopted repositories
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: pattern
// in: query
// description: pattern of repositories to search for
// type: string
// responses:
// "200":
// "$ref": "#/responses/StringSlice"
// "403":
// "$ref": "#/responses/forbidden"
listOptions := utils.GetListOptions(ctx)
if listOptions.Page == 0 {
listOptions.Page = 1
}
repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, ctx.FormString("query"), &listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, repoNames)
}
// AdoptRepository will adopt an unadopted repository
func AdoptRepository(ctx *context.APIContext) {
// swagger:operation POST /admin/unadopted/{owner}/{repo} admin adminAdoptRepository
// ---
// summary: Adopt unadopted files as a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "403":
// "$ref": "#/responses/forbidden"
ownerName := ctx.PathParam("username")
repoName := ctx.PathParam("reponame")
ctxUser, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
// check not a repo
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName)))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if has || !exist {
ctx.APIErrorNotFound()
return
}
if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{
Name: repoName,
IsPrivate: true,
}); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteUnadoptedRepository will delete an unadopted repository
func DeleteUnadoptedRepository(ctx *context.APIContext) {
// swagger:operation DELETE /admin/unadopted/{owner}/{repo} admin adminDeleteUnadoptedRepository
// ---
// summary: Delete unadopted files
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
ownerName := ctx.PathParam("username")
repoName := ctx.PathParam("reponame")
ctxUser, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
// check not a repo
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName)))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if has || !exist {
ctx.APIErrorNotFound()
return
}
if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, repoName); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
"gitea.dev/modules/log"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/cron"
)
// ListCronTasks api for getting cron tasks
func ListCronTasks(ctx *context.APIContext) {
// swagger:operation GET /admin/cron admin adminCronList
// ---
// summary: List cron tasks
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/CronList"
// "403":
// "$ref": "#/responses/forbidden"
tasks := cron.ListTasks()
count := len(tasks)
listOpts := utils.GetListOptions(ctx)
tasks = util.PaginateSlice(tasks, listOpts.Page, listOpts.PageSize).(cron.TaskTable)
res := make([]structs.Cron, len(tasks))
for i, task := range tasks {
res[i] = structs.Cron{
Name: task.Name,
Schedule: task.Spec,
Next: task.Next,
Prev: task.Prev,
ExecTimes: task.ExecTimes,
}
}
ctx.SetTotalCountHeader(int64(count))
ctx.JSON(http.StatusOK, res)
}
// PostCronTask api for getting cron tasks
func PostCronTask(ctx *context.APIContext) {
// swagger:operation POST /admin/cron/{task} admin adminCronRun
// ---
// summary: Run cron task
// produces:
// - application/json
// parameters:
// - name: task
// in: path
// description: task to run
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
task := cron.GetTask(ctx.PathParam("task"))
if task == nil {
ctx.APIErrorNotFound()
return
}
task.Run()
log.Trace("Cron Task %s started by admin(%s)", task.Name, ctx.Doer.Name)
ctx.Status(http.StatusNoContent)
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetAllEmails
func GetAllEmails(ctx *context.APIContext) {
// swagger:operation GET /admin/emails admin adminGetAllEmails
// ---
// summary: List all emails
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/EmailList"
// "403":
// "$ref": "#/responses/forbidden"
listOptions := utils.GetListOptions(ctx)
emails, maxResults, err := user_model.SearchEmails(ctx, &user_model.SearchEmailOptions{
Keyword: ctx.PathParam("email"),
ListOptions: listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
results := make([]*api.Email, len(emails))
for i := range emails {
results[i] = convert.ToEmailSearch(emails[i])
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
// SearchEmail
func SearchEmail(ctx *context.APIContext) {
// swagger:operation GET /admin/emails/search admin adminSearchEmails
// ---
// summary: Search all emails
// produces:
// - application/json
// parameters:
// - name: q
// in: query
// description: keyword
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/EmailList"
// "403":
// "$ref": "#/responses/forbidden"
ctx.SetPathParam("email", ctx.FormTrim("q"))
GetAllEmails(ctx)
}
+204
View File
@@ -0,0 +1,204 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"errors"
"net/http"
"gitea.dev/models/webhook"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
webhook_service "gitea.dev/services/webhook"
)
// ListHooks list system's webhooks
func ListHooks(ctx *context.APIContext) {
// swagger:operation GET /admin/hooks admin adminListHooks
// ---
// summary: List system's webhooks
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - type: string
// enum:
// - system
// - default
// - all
// description: system, default or both kinds of webhooks
// name: type
// default: system
// in: query
//
// responses:
// "200":
// "$ref": "#/responses/HookList"
// for compatibility the default value is true
isSystemWebhook := optional.Some(true)
typeValue := ctx.FormString("type")
switch typeValue {
case "default":
isSystemWebhook = optional.Some(false)
case "all":
isSystemWebhook = optional.None[bool]()
}
listOptions := utils.GetListOptions(ctx)
opts := &webhook.ListSystemWebhookOptions{
ListOptions: listOptions,
IsSystem: isSystemWebhook,
}
sysHooks, total, err := webhook.GetGlobalWebhooks(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
hooks := make([]*api.Hook, len(sysHooks))
for i, hook := range sysHooks {
h, err := webhook_service.ToHook(setting.AppURL+"/-/admin", hook)
if err != nil {
ctx.APIErrorInternal(err)
return
}
hooks[i] = h
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, hooks)
}
// GetHook get an organization's hook by id
func GetHook(ctx *context.APIContext) {
// swagger:operation GET /admin/hooks/{id} admin adminGetHook
// ---
// summary: Get a hook
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the hook to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Hook"
hookID := ctx.PathParamInt64("id")
hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
h, err := webhook_service.ToHook("/-/admin/", hook)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, h)
}
// CreateHook create a hook for an organization
func CreateHook(ctx *context.APIContext) {
// swagger:operation POST /admin/hooks admin adminCreateHook
// ---
// summary: Create a hook
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreateHookOption"
// responses:
// "201":
// "$ref": "#/responses/Hook"
form := web.GetForm(ctx).(*api.CreateHookOption)
utils.AddSystemHook(ctx, form)
}
// EditHook modify a hook of a repository
func EditHook(ctx *context.APIContext) {
// swagger:operation PATCH /admin/hooks/{id} admin adminEditHook
// ---
// summary: Update a hook
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the hook to update
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditHookOption"
// responses:
// "200":
// "$ref": "#/responses/Hook"
form := web.GetForm(ctx).(*api.EditHookOption)
// TODO in body params
hookID := ctx.PathParamInt64("id")
utils.EditSystemHook(ctx, form, hookID)
}
// DeleteHook delete a system hook
func DeleteHook(ctx *context.APIContext) {
// swagger:operation DELETE /admin/hooks/{id} admin adminDeleteHook
// ---
// summary: Delete a hook
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the hook to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
hookID := ctx.PathParamInt64("id")
if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
"gitea.dev/models/db"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// CreateOrg api for create organization
func CreateOrg(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/orgs admin adminCreateOrg
// ---
// summary: Create an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user who will own the created organization
// type: string
// required: true
// - name: organization
// in: body
// required: true
// schema: { "$ref": "#/definitions/CreateOrgOption" }
// responses:
// "201":
// "$ref": "#/responses/Organization"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateOrgOption)
visibility := api.VisibleTypePublic
if form.Visibility != "" {
visibility = api.VisibilityModes[string(form.Visibility)]
}
org := &organization.Organization{
Name: form.UserName,
FullName: form.FullName,
Description: form.Description,
Website: form.Website,
Location: form.Location,
IsActive: true,
Type: user_model.UserTypeOrganization,
Visibility: visibility,
}
if err := organization.CreateOrganization(ctx, org, ctx.ContextUser); err != nil {
if user_model.IsErrUserAlreadyExist(err) ||
db.IsErrNameReserved(err) ||
db.IsErrNameCharsNotAllowed(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusCreated, convert.ToOrganization(ctx, org))
}
// GetAllOrgs API for getting information of all the organizations
func GetAllOrgs(ctx *context.APIContext) {
// swagger:operation GET /admin/orgs admin adminGetAllOrgs
// ---
// summary: List all organizations
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/OrganizationList"
// "403":
// "$ref": "#/responses/forbidden"
listOptions := utils.GetListOptions(ctx)
users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Types: []user_model.UserType{user_model.UserTypeOrganization},
OrderBy: db.SearchOrderByAlphabetically,
ListOptions: listOptions,
Visible: []api.VisibleType{api.VisibleTypePublic, api.VisibleTypeLimited, api.VisibleTypePrivate},
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
orgs := make([]*api.Organization, len(users))
for i := range users {
orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(users[i]))
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &orgs)
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/repo"
"gitea.dev/services/context"
)
// CreateRepo api for creating a repository
func CreateRepo(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/repos admin adminCreateRepo
// ---
// summary: Create a repository on behalf of a user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user who will own the created repository
// type: string
// required: true
// - name: repository
// in: body
// required: true
// schema: { "$ref": "#/definitions/CreateRepoOption" }
// responses:
// "201":
// "$ref": "#/responses/Repository"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateRepoOption)
repo.CreateUserRepo(ctx, ctx.ContextUser, *form)
}
+126
View File
@@ -0,0 +1,126 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// CreateRegistrationToken returns the token to register global runners
func CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /admin/actions/runners/registration-token admin adminCreateRunnerRegistrationToken
// ---
// summary: Get a global actions runner registration token
// produces:
// - application/json
// parameters:
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, 0, 0)
}
// ListRunners get all runners
func ListRunners(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/runners admin getAdminRunners
// ---
// summary: Get all runners
// produces:
// - application/json
// parameters:
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRunners(ctx, 0, 0)
}
// GetRunner get a global runner
func GetRunner(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/runners/{runner_id} admin getAdminRunner
// ---
// summary: Get a global runner
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.GetRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete a global runner
func DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /admin/actions/runners/{runner_id} admin deleteAdminRunner
// ---
// summary: Delete a global runner
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "204":
// description: runner has been deleted
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
// UpdateRunner update a global runner
func UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /admin/actions/runners/{runner_id} admin updateAdminRunner
// ---
// summary: Update a global runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
+570
View File
@@ -0,0 +1,570 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"errors"
"fmt"
"net/http"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/auth"
"gitea.dev/models/db"
org_model "gitea.dev/models/organization"
packages_model "gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/auth/password"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/mailer"
user_service "gitea.dev/services/user"
)
func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64) {
if sourceID == 0 {
return
}
source, err := auth.GetSourceByID(ctx, sourceID)
if err != nil {
if auth.IsErrSourceNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
u.LoginType = source.Type
u.LoginSource = source.ID
}
// CreateUser create a user
func CreateUser(ctx *context.APIContext) {
// swagger:operation POST /admin/users admin adminCreateUser
// ---
// summary: Create a user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateUserOption"
// responses:
// "201":
// "$ref": "#/responses/User"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateUserOption)
u := &user_model.User{
Name: form.Username,
FullName: form.FullName,
Email: form.Email,
Passwd: form.Password,
MustChangePassword: true,
LoginType: auth.Plain,
LoginName: form.LoginName,
}
if form.MustChangePassword != nil {
u.MustChangePassword = *form.MustChangePassword
}
parseAuthSource(ctx, u, form.SourceID)
if ctx.Written() {
return
}
if u.LoginType == auth.Plain {
if len(form.Password) < setting.MinPasswordLength {
err := errors.New("PasswordIsRequired")
ctx.APIError(http.StatusBadRequest, err)
return
}
if !password.IsComplexEnough(form.Password) {
err := errors.New("PasswordComplexity")
ctx.APIError(http.StatusBadRequest, err)
return
}
if err := password.IsPwned(ctx, form.Password); err != nil {
if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
}
ctx.APIError(http.StatusBadRequest, errors.New("PasswordPwned"))
return
}
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsActive: optional.Some(true),
IsRestricted: optional.FromPtr(form.Restricted),
}
if form.Visibility != "" {
visibility := api.VisibilityModes[string(form.Visibility)]
overwriteDefault.Visibility = &visibility
}
// Update the user creation timestamp. This can only be done after the user
// record has been inserted into the database; the insert intself will always
// set the creation timestamp to "now".
if form.Created != nil {
u.CreatedUnix = timeutil.TimeStamp(form.Created.Unix())
u.UpdatedUnix = u.CreatedUnix
}
if err := user_model.AdminCreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
if user_model.IsErrUserAlreadyExist(err) ||
user_model.IsErrEmailAlreadyUsed(err) ||
db.IsErrNameReserved(err) ||
db.IsErrNameCharsNotAllowed(err) ||
user_model.IsErrEmailCharIsNotSupported(err) ||
user_model.IsErrEmailInvalid(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !user_model.IsEmailDomainAllowed(u.Email) {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
}
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification.
if form.SendNotify {
mailer.SendRegisterNotifyMail(u)
}
ctx.JSON(http.StatusCreated, convert.ToUser(ctx, u, ctx.Doer))
}
// EditUser api for modifying a user's information
func EditUser(ctx *context.APIContext) {
// swagger:operation PATCH /admin/users/{username} admin adminEditUser
// ---
// summary: Edit an existing user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose data is to be edited
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditUserOption"
// responses:
// "200":
// "$ref": "#/responses/User"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditUserOption)
authOpts := &user_service.UpdateAuthOptions{
LoginSource: optional.FromNonDefault(form.SourceID),
LoginName: optional.Some(form.LoginName),
Password: optional.FromNonDefault(form.Password),
MustChangePassword: optional.FromPtr(form.MustChangePassword),
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
}
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.APIError(http.StatusBadRequest, fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
case errors.Is(err, password.ErrComplexity):
ctx.APIError(http.StatusBadRequest, err)
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
ctx.APIError(http.StatusBadRequest, err)
default:
ctx.APIErrorInternal(err)
}
return
}
if form.Email != nil {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
if !user_model.IsEmailDomainAllowed(*form.Email) {
err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)
}
ctx.APIError(http.StatusBadRequest, err)
case user_model.IsErrEmailAlreadyUsed(err):
ctx.APIError(http.StatusBadRequest, err)
default:
ctx.APIErrorInternal(err)
}
return
}
}
opts := &user_service.UpdateOptions{
FullName: optional.FromPtr(form.FullName),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
Visibility: optional.FromMapLookup(api.VisibilityModes, string(form.Visibility)),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
IsRestricted: optional.FromPtr(form.Restricted),
}
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
if user_model.IsErrDeleteLastAdminUser(err) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer))
}
// DeleteUser api for deleting a user
func DeleteUser(ctx *context.APIContext) {
// swagger:operation DELETE /admin/users/{username} admin adminDeleteUser
// ---
// summary: Delete a user
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user to delete
// type: string
// required: true
// - name: purge
// in: query
// description: purge the user from the system completely
// type: boolean
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
if ctx.ContextUser.IsOrganization() {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
return
}
// admin should not delete themself
if ctx.ContextUser.ID == ctx.Doer.ID {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("you cannot delete yourself"))
return
}
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
if repo_model.IsErrUserOwnRepos(err) ||
org_model.IsErrUserHasOrgs(err) ||
packages_model.IsErrUserOwnPackages(err) ||
user_model.IsErrDeleteLastAdminUser(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
log.Trace("Account deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
ctx.Status(http.StatusNoContent)
}
// CreatePublicKey api for creating a public key to a user
func CreatePublicKey(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/keys admin adminCreatePublicKey
// ---
// summary: Add a public key on behalf of a user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user who is to receive a public key
// type: string
// required: true
// - name: key
// in: body
// schema:
// "$ref": "#/definitions/CreateKeyOption"
// responses:
// "201":
// "$ref": "#/responses/PublicKey"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateKeyOption)
user.CreateUserPublicKey(ctx, *form, ctx.ContextUser.ID)
}
// DeleteUserPublicKey api for deleting a user's public key
func DeleteUserPublicKey(ctx *context.APIContext) {
// swagger:operation DELETE /admin/users/{username}/keys/{id} admin adminDeleteUserPublicKey
// ---
// summary: Delete a user's public key
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose public key is to be deleted
// type: string
// required: true
// - name: id
// in: path
// description: id of the key to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.PathParamInt64("id")); err != nil {
if asymkey_model.IsErrKeyNotExist(err) {
ctx.APIErrorNotFound()
} else if asymkey_model.IsErrKeyAccessDenied(err) {
ctx.APIError(http.StatusForbidden, "You do not have access to this key")
} else {
ctx.APIErrorInternal(err)
}
return
}
log.Trace("Key deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
ctx.Status(http.StatusNoContent)
}
// SearchUsers API for getting information of the users according the filter conditions
func SearchUsers(ctx *context.APIContext) {
// swagger:operation GET /admin/users admin adminSearchUsers
// ---
// summary: Search users according filter conditions
// produces:
// - application/json
// parameters:
// - name: source_id
// in: query
// description: ID of the user's login source to search for
// type: integer
// format: int64
// - name: login_name
// in: query
// description: identifier of the user, provided by the external authenticator
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: sort
// in: query
// description: sort users by attribute. Supported values are
// "name", "created", "updated" and "id".
// Default is "name"
// type: string
// - name: order
// in: query
// description: sort order, either "asc" (ascending) or "desc" (descending).
// Default is "asc", ignored if "sort" is not specified.
// type: string
// - name: q
// in: query
// description: search term (username, full name, email)
// type: string
// - name: visibility
// in: query
// description: visibility filter. Supported values are
// "public", "limited" and "private".
// type: string
// - name: is_active
// in: query
// description: filter active users
// type: boolean
// - name: is_admin
// in: query
// description: filter admin users
// type: boolean
// - name: is_restricted
// in: query
// description: filter restricted users
// type: boolean
// - name: is_2fa_enabled
// in: query
// description: filter 2FA enabled users
// type: boolean
// - name: is_prohibit_login
// in: query
// description: filter login prohibited users
// type: boolean
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
listOptions := utils.GetListOptions(ctx)
orderBy, ok := utils.ResolveSortOrder(ctx, user_model.AdminUserOrderByMap, db.SearchOrderByAlphabetically)
if !ok {
return
}
var visible []api.VisibleType
visibilityParam := ctx.FormString("visibility")
if len(visibilityParam) > 0 {
if visibility, ok := api.VisibilityModes[visibilityParam]; ok {
visible = []api.VisibleType{visibility}
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam))
return
}
}
searchOpts := user_model.SearchUserOptions{
Actor: ctx.Doer,
Types: []user_model.UserType{user_model.UserTypeIndividual},
LoginName: ctx.FormTrim("login_name"),
SourceID: ctx.FormInt64("source_id"),
Keyword: ctx.FormTrim("q"),
Visible: visible,
OrderBy: orderBy,
ListOptions: listOptions,
SearchByEmail: true,
}
if ctx.FormString("is_active") != "" {
searchOpts.IsActive = optional.Some(ctx.FormBool("is_active"))
}
if ctx.FormString("is_admin") != "" {
searchOpts.IsAdmin = optional.Some(ctx.FormBool("is_admin"))
}
if ctx.FormString("is_restricted") != "" {
searchOpts.IsRestricted = optional.Some(ctx.FormBool("is_restricted"))
}
if ctx.FormString("is_2fa_enabled") != "" {
searchOpts.IsTwoFactorEnabled = optional.Some(ctx.FormBool("is_2fa_enabled"))
}
if ctx.FormString("is_prohibit_login") != "" {
searchOpts.IsProhibitLogin = optional.Some(ctx.FormBool("is_prohibit_login"))
}
users, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
results := make([]*api.User, len(users))
for i := range users {
results[i] = convert.ToUser(ctx, users[i], ctx.Doer)
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
// RenameUser api for renaming a user
func RenameUser(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/rename admin adminRenameUser
// ---
// summary: Rename a user
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: current username of the user
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/RenameUserOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
if ctx.ContextUser.IsOrganization() {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
return
}
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
// Check if username has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
)
// ListUserBadges lists all badges belonging to a user
func ListUserBadges(ctx *context.APIContext) {
// swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges
// ---
// summary: List a user's badges
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose badges are to be listed
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/BadgeList"
// "404":
// "$ref": "#/responses/notFound"
badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &badges)
}
// AddUserBadges add badges to a user
func AddUserBadges(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges
// ---
// summary: Add a badge to a user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user to whom a badge is to be added
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UserBadgeOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*api.UserBadgeOption)
badges := prepareBadgesForReplaceOrAdd(*form)
if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteUserBadges delete a badge from a user
func DeleteUserBadges(ctx *context.APIContext) {
// swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges
// ---
// summary: Remove a badge from a user
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose badge is to be deleted
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UserBadgeOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.UserBadgeOption)
badges := prepareBadgesForReplaceOrAdd(*form)
if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func prepareBadgesForReplaceOrAdd(form api.UserBadgeOption) []*user_model.Badge {
badges := make([]*user_model.Badge, len(form.BadgeSlugs))
for i, badge := range form.BadgeSlugs {
badges[i] = &user_model.Badge{
Slug: badge,
}
}
return badges
}