summaryrefslogtreecommitdiffstats
path: root/routers/api/v1/admin
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /routers/api/v1/admin
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--routers/api/v1/admin/adopt.go180
-rw-r--r--routers/api/v1/admin/cron.go86
-rw-r--r--routers/api/v1/admin/email.go87
-rw-r--r--routers/api/v1/admin/hooks.go176
-rw-r--r--routers/api/v1/admin/org.go123
-rw-r--r--routers/api/v1/admin/quota.go53
-rw-r--r--routers/api/v1/admin/quota_group.go436
-rw-r--r--routers/api/v1/admin/quota_rule.go219
-rw-r--r--routers/api/v1/admin/repo.go49
-rw-r--r--routers/api/v1/admin/runners.go26
-rw-r--r--routers/api/v1/admin/user.go509
11 files changed, 1944 insertions, 0 deletions
diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go
new file mode 100644
index 0000000..a4708fe
--- /dev/null
+++ b/routers/api/v1/admin/adopt.go
@@ -0,0 +1,180 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "net/http"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
+ repo_service "code.gitea.io/gitea/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.InternalServerError(err)
+ return
+ }
+
+ ctx.SetTotalCountHeader(int64(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.Params(":username")
+ repoName := ctx.Params(":reponame")
+
+ ctxUser, err := user_model.GetUserByName(ctx, ownerName)
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.NotFound()
+ return
+ }
+ ctx.InternalServerError(err)
+ return
+ }
+
+ // check not a repo
+ has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName))
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ if has || !isDir {
+ ctx.NotFound()
+ return
+ }
+ if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{
+ Name: repoName,
+ IsPrivate: true,
+ }); err != nil {
+ ctx.InternalServerError(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.Params(":username")
+ repoName := ctx.Params(":reponame")
+
+ ctxUser, err := user_model.GetUserByName(ctx, ownerName)
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.NotFound()
+ return
+ }
+ ctx.InternalServerError(err)
+ return
+ }
+
+ // check not a repo
+ has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName))
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ if has || !isDir {
+ ctx.NotFound()
+ return
+ }
+
+ if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, repoName); err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/admin/cron.go b/routers/api/v1/admin/cron.go
new file mode 100644
index 0000000..e1ca604
--- /dev/null
+++ b/routers/api/v1/admin/cron.go
@@ -0,0 +1,86 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/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.Params(":task"))
+ if task == nil {
+ ctx.NotFound()
+ return
+ }
+ task.Run()
+ log.Trace("Cron Task %s started by admin(%s)", task.Name, ctx.Doer.Name)
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/admin/email.go b/routers/api/v1/admin/email.go
new file mode 100644
index 0000000..ba963e9
--- /dev/null
+++ b/routers/api/v1/admin/email.go
@@ -0,0 +1,87 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "net/http"
+
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/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.Params(":email"),
+ ListOptions: listOptions,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetAllEmails", err)
+ return
+ }
+
+ results := make([]*api.Email, len(emails))
+ for i := range emails {
+ results[i] = convert.ToEmailSearch(emails[i])
+ }
+
+ ctx.SetLinkHeader(int(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.SetParams(":email", ctx.FormTrim("q"))
+ GetAllEmails(ctx)
+}
diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go
new file mode 100644
index 0000000..b246cb6
--- /dev/null
+++ b/routers/api/v1/admin/hooks.go
@@ -0,0 +1,176 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "errors"
+ "net/http"
+
+ "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
+ webhook_service "code.gitea.io/gitea/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
+ // responses:
+ // "200":
+ // "$ref": "#/responses/HookList"
+
+ sysHooks, err := webhook.GetSystemWebhooks(ctx, false)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", 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.Error(http.StatusInternalServerError, "convert.ToHook", err)
+ return
+ }
+ hooks[i] = h
+ }
+ 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.ParamsInt64(":id")
+ hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
+ }
+ return
+ }
+ h, err := webhook_service.ToHook("/admin/", hook)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "convert.ToHook", 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.ParamsInt64(":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.ParamsInt64(":id")
+ if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteDefaultSystemWebhook", err)
+ }
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go
new file mode 100644
index 0000000..a5c299b
--- /dev/null
+++ b/routers/api/v1/admin/org.go
@@ -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"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/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 that 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[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.Error(http.StatusUnprocessableEntity, "", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateOrganization", 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,
+ Type: user_model.UserTypeOrganization,
+ OrderBy: db.SearchOrderByAlphabetically,
+ ListOptions: listOptions,
+ Visible: []api.VisibleType{api.VisibleTypePublic, api.VisibleTypeLimited, api.VisibleTypePrivate},
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "SearchOrganizations", err)
+ return
+ }
+ orgs := make([]*api.Organization, len(users))
+ for i := range users {
+ orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(users[i]))
+ }
+
+ ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
+ ctx.SetTotalCountHeader(maxResults)
+ ctx.JSON(http.StatusOK, &orgs)
+}
diff --git a/routers/api/v1/admin/quota.go b/routers/api/v1/admin/quota.go
new file mode 100644
index 0000000..1e7c11e
--- /dev/null
+++ b/routers/api/v1/admin/quota.go
@@ -0,0 +1,53 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "net/http"
+
+ quota_model "code.gitea.io/gitea/models/quota"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/convert"
+)
+
+// GetUserQuota return information about a user's quota
+func GetUserQuota(ctx *context.APIContext) {
+ // swagger:operation GET /admin/users/{username}/quota admin adminGetUserQuota
+ // ---
+ // summary: Get the user's quota info
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of user to query
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/QuotaInfo"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ used, err := quota_model.GetUsedForUser(ctx, ctx.ContextUser.ID)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "quota_model.GetUsedForUser", err)
+ return
+ }
+
+ groups, err := quota_model.GetGroupsForUser(ctx, ctx.ContextUser.ID)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "quota_model.GetGroupsForUser", err)
+ return
+ }
+
+ result := convert.ToQuotaInfo(used, groups, true)
+ ctx.JSON(http.StatusOK, &result)
+}
diff --git a/routers/api/v1/admin/quota_group.go b/routers/api/v1/admin/quota_group.go
new file mode 100644
index 0000000..e20b361
--- /dev/null
+++ b/routers/api/v1/admin/quota_group.go
@@ -0,0 +1,436 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ go_context "context"
+ "net/http"
+
+ "code.gitea.io/gitea/models/db"
+ quota_model "code.gitea.io/gitea/models/quota"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/convert"
+)
+
+// ListQuotaGroups returns all the quota groups
+func ListQuotaGroups(ctx *context.APIContext) {
+ // swagger:operation GET /admin/quota/groups admin adminListQuotaGroups
+ // ---
+ // summary: List the available quota groups
+ // produces:
+ // - application/json
+ // responses:
+ // "200":
+ // "$ref": "#/responses/QuotaGroupList"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ groups, err := quota_model.ListGroups(ctx)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "quota_model.ListGroups", err)
+ return
+ }
+ for _, group := range groups {
+ if err = group.LoadRules(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "quota_model.group.LoadRules", err)
+ return
+ }
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToQuotaGroupList(groups, true))
+}
+
+func createQuotaGroupWithRules(ctx go_context.Context, opts *api.CreateQuotaGroupOptions) (*quota_model.Group, error) {
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer committer.Close()
+
+ group, err := quota_model.CreateGroup(ctx, opts.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, rule := range opts.Rules {
+ exists, err := quota_model.DoesRuleExist(ctx, rule.Name)
+ if err != nil {
+ return nil, err
+ }
+ if !exists {
+ var limit int64
+ if rule.Limit != nil {
+ limit = *rule.Limit
+ }
+
+ subjects, err := toLimitSubjects(rule.Subjects)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = quota_model.CreateRule(ctx, rule.Name, limit, *subjects)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if err = group.AddRuleByName(ctx, rule.Name); err != nil {
+ return nil, err
+ }
+ }
+
+ if err = group.LoadRules(ctx); err != nil {
+ return nil, err
+ }
+
+ return group, committer.Commit()
+}
+
+// CreateQuotaGroup creates a new quota group
+func CreateQuotaGroup(ctx *context.APIContext) {
+ // swagger:operation POST /admin/quota/groups admin adminCreateQuotaGroup
+ // ---
+ // summary: Create a new quota group
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: group
+ // in: body
+ // description: Definition of the quota group
+ // schema:
+ // "$ref": "#/definitions/CreateQuotaGroupOptions"
+ // required: true
+ // responses:
+ // "201":
+ // "$ref": "#/responses/QuotaGroup"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "409":
+ // "$ref": "#/responses/error"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ form := web.GetForm(ctx).(*api.CreateQuotaGroupOptions)
+
+ group, err := createQuotaGroupWithRules(ctx, form)
+ if err != nil {
+ if quota_model.IsErrGroupAlreadyExists(err) {
+ ctx.Error(http.StatusConflict, "", err)
+ } else if quota_model.IsErrParseLimitSubjectUnrecognized(err) {
+ ctx.Error(http.StatusUnprocessableEntity, "", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "quota_model.CreateGroup", err)
+ }
+ return
+ }
+ ctx.JSON(http.StatusCreated, convert.ToQuotaGroup(*group, true))
+}
+
+// ListUsersInQuotaGroup lists all the users in a quota group
+func ListUsersInQuotaGroup(ctx *context.APIContext) {
+ // swagger:operation GET /admin/quota/groups/{quotagroup}/users admin adminListUsersInQuotaGroup
+ // ---
+ // summary: List users in a quota group
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotagroup
+ // in: path
+ // description: quota group to list members of
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/UserList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ users, err := quota_model.ListUsersInGroup(ctx, ctx.QuotaGroup.Name)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "quota_model.ListUsersInGroup", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, users))
+}
+
+// AddUserToQuotaGroup adds a user to a quota group
+func AddUserToQuotaGroup(ctx *context.APIContext) {
+ // swagger:operation PUT /admin/quota/groups/{quotagroup}/users/{username} admin adminAddUserToQuotaGroup
+ // ---
+ // summary: Add a user to a quota group
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotagroup
+ // in: path
+ // description: quota group to add the user to
+ // type: string
+ // required: true
+ // - name: username
+ // in: path
+ // description: username of the user to add to the quota group
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "409":
+ // "$ref": "#/responses/error"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ err := ctx.QuotaGroup.AddUserByID(ctx, ctx.ContextUser.ID)
+ if err != nil {
+ if quota_model.IsErrUserAlreadyInGroup(err) {
+ ctx.Error(http.StatusConflict, "", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "quota_group.group.AddUserByID", err)
+ }
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
+
+// RemoveUserFromQuotaGroup removes a user from a quota group
+func RemoveUserFromQuotaGroup(ctx *context.APIContext) {
+ // swagger:operation DELETE /admin/quota/groups/{quotagroup}/users/{username} admin adminRemoveUserFromQuotaGroup
+ // ---
+ // summary: Remove a user from a quota group
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotagroup
+ // in: path
+ // description: quota group to remove a user from
+ // type: string
+ // required: true
+ // - name: username
+ // in: path
+ // description: username of the user to remove from the quota group
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ err := ctx.QuotaGroup.RemoveUserByID(ctx, ctx.ContextUser.ID)
+ if err != nil {
+ if quota_model.IsErrUserNotInGroup(err) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "quota_model.group.RemoveUserByID", err)
+ }
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
+
+// SetUserQuotaGroups moves the user to specific quota groups
+func SetUserQuotaGroups(ctx *context.APIContext) {
+ // swagger:operation POST /admin/users/{username}/quota/groups admin adminSetUserQuotaGroups
+ // ---
+ // summary: Set the user's quota groups to a given list.
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of the user to modify the quota groups from
+ // type: string
+ // required: true
+ // - name: groups
+ // in: body
+ // description: list of groups that the user should be a member of
+ // schema:
+ // "$ref": "#/definitions/SetUserQuotaGroupsOptions"
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ form := web.GetForm(ctx).(*api.SetUserQuotaGroupsOptions)
+
+ err := quota_model.SetUserGroups(ctx, ctx.ContextUser.ID, form.Groups)
+ if err != nil {
+ if quota_model.IsErrGroupNotFound(err) {
+ ctx.Error(http.StatusUnprocessableEntity, "", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "quota_model.SetUserGroups", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// DeleteQuotaGroup deletes a quota group
+func DeleteQuotaGroup(ctx *context.APIContext) {
+ // swagger:operation DELETE /admin/quota/groups/{quotagroup} admin adminDeleteQuotaGroup
+ // ---
+ // summary: Delete a quota group
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotagroup
+ // in: path
+ // description: quota group to delete
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ err := quota_model.DeleteGroupByName(ctx, ctx.QuotaGroup.Name)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "quota_model.DeleteGroupByName", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// GetQuotaGroup returns information about a quota group
+func GetQuotaGroup(ctx *context.APIContext) {
+ // swagger:operation GET /admin/quota/groups/{quotagroup} admin adminGetQuotaGroup
+ // ---
+ // summary: Get information about the quota group
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotagroup
+ // in: path
+ // description: quota group to query
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/QuotaGroup"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ ctx.JSON(http.StatusOK, convert.ToQuotaGroup(*ctx.QuotaGroup, true))
+}
+
+// AddRuleToQuotaGroup adds a rule to a quota group
+func AddRuleToQuotaGroup(ctx *context.APIContext) {
+ // swagger:operation PUT /admin/quota/groups/{quotagroup}/rules/{quotarule} admin adminAddRuleToQuotaGroup
+ // ---
+ // summary: Adds a rule to a quota group
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotagroup
+ // in: path
+ // description: quota group to add a rule to
+ // type: string
+ // required: true
+ // - name: quotarule
+ // in: path
+ // description: the name of the quota rule to add to the group
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "409":
+ // "$ref": "#/responses/error"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ err := ctx.QuotaGroup.AddRuleByName(ctx, ctx.QuotaRule.Name)
+ if err != nil {
+ if quota_model.IsErrRuleAlreadyInGroup(err) {
+ ctx.Error(http.StatusConflict, "", err)
+ } else if quota_model.IsErrRuleNotFound(err) {
+ ctx.Error(http.StatusUnprocessableEntity, "", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "quota_model.group.AddRuleByName", err)
+ }
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
+
+// RemoveRuleFromQuotaGroup removes a rule from a quota group
+func RemoveRuleFromQuotaGroup(ctx *context.APIContext) {
+ // swagger:operation DELETE /admin/quota/groups/{quotagroup}/rules/{quotarule} admin adminRemoveRuleFromQuotaGroup
+ // ---
+ // summary: Removes a rule from a quota group
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotagroup
+ // in: path
+ // description: quota group to remove a rule from
+ // type: string
+ // required: true
+ // - name: quotarule
+ // in: path
+ // description: the name of the quota rule to remove from the group
+ // type: string
+ // required: true
+ // responses:
+ // "201":
+ // "$ref": "#/responses/empty"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ err := ctx.QuotaGroup.RemoveRuleByName(ctx, ctx.QuotaRule.Name)
+ if err != nil {
+ if quota_model.IsErrRuleNotInGroup(err) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "quota_model.group.RemoveRuleByName", err)
+ }
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/admin/quota_rule.go b/routers/api/v1/admin/quota_rule.go
new file mode 100644
index 0000000..85c05e1
--- /dev/null
+++ b/routers/api/v1/admin/quota_rule.go
@@ -0,0 +1,219 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "fmt"
+ "net/http"
+
+ quota_model "code.gitea.io/gitea/models/quota"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/convert"
+)
+
+func toLimitSubjects(subjStrings []string) (*quota_model.LimitSubjects, error) {
+ subjects := make(quota_model.LimitSubjects, len(subjStrings))
+ for i := range len(subjStrings) {
+ subj, err := quota_model.ParseLimitSubject(subjStrings[i])
+ if err != nil {
+ return nil, err
+ }
+ subjects[i] = subj
+ }
+
+ return &subjects, nil
+}
+
+// ListQuotaRules lists all the quota rules
+func ListQuotaRules(ctx *context.APIContext) {
+ // swagger:operation GET /admin/quota/rules admin adminListQuotaRules
+ // ---
+ // summary: List the available quota rules
+ // produces:
+ // - application/json
+ // responses:
+ // "200":
+ // "$ref": "#/responses/QuotaRuleInfoList"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ rules, err := quota_model.ListRules(ctx)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "quota_model.ListQuotaRules", err)
+ return
+ }
+
+ result := make([]api.QuotaRuleInfo, len(rules))
+ for i := range len(rules) {
+ result[i] = convert.ToQuotaRuleInfo(rules[i], true)
+ }
+
+ ctx.JSON(http.StatusOK, result)
+}
+
+// CreateQuotaRule creates a new quota rule
+func CreateQuotaRule(ctx *context.APIContext) {
+ // swagger:operation POST /admin/quota/rules admin adminCreateQuotaRule
+ // ---
+ // summary: Create a new quota rule
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: rule
+ // in: body
+ // description: Definition of the quota rule
+ // schema:
+ // "$ref": "#/definitions/CreateQuotaRuleOptions"
+ // required: true
+ // responses:
+ // "201":
+ // "$ref": "#/responses/QuotaRuleInfo"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "409":
+ // "$ref": "#/responses/error"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ form := web.GetForm(ctx).(*api.CreateQuotaRuleOptions)
+
+ if form.Limit == nil {
+ ctx.Error(http.StatusUnprocessableEntity, "quota_model.ParseLimitSubject", fmt.Errorf("[Limit]: Required"))
+ return
+ }
+
+ subjects, err := toLimitSubjects(form.Subjects)
+ if err != nil {
+ ctx.Error(http.StatusUnprocessableEntity, "quota_model.ParseLimitSubject", err)
+ return
+ }
+
+ rule, err := quota_model.CreateRule(ctx, form.Name, *form.Limit, *subjects)
+ if err != nil {
+ if quota_model.IsErrRuleAlreadyExists(err) {
+ ctx.Error(http.StatusConflict, "", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "quota_model.CreateRule", err)
+ }
+ return
+ }
+ ctx.JSON(http.StatusCreated, convert.ToQuotaRuleInfo(*rule, true))
+}
+
+// GetQuotaRule returns information about the specified quota rule
+func GetQuotaRule(ctx *context.APIContext) {
+ // swagger:operation GET /admin/quota/rules/{quotarule} admin adminGetQuotaRule
+ // ---
+ // summary: Get information about a quota rule
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotarule
+ // in: path
+ // description: quota rule to query
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/QuotaRuleInfo"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ ctx.JSON(http.StatusOK, convert.ToQuotaRuleInfo(*ctx.QuotaRule, true))
+}
+
+// EditQuotaRule changes an existing quota rule
+func EditQuotaRule(ctx *context.APIContext) {
+ // swagger:operation PATCH /admin/quota/rules/{quotarule} admin adminEditQuotaRule
+ // ---
+ // summary: Change an existing quota rule
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotarule
+ // in: path
+ // description: Quota rule to change
+ // type: string
+ // required: true
+ // - name: rule
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/EditQuotaRuleOptions"
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/QuotaRuleInfo"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ form := web.GetForm(ctx).(*api.EditQuotaRuleOptions)
+
+ var subjects *quota_model.LimitSubjects
+ if form.Subjects != nil {
+ subjs := make(quota_model.LimitSubjects, len(*form.Subjects))
+ for i := range len(*form.Subjects) {
+ subj, err := quota_model.ParseLimitSubject((*form.Subjects)[i])
+ if err != nil {
+ ctx.Error(http.StatusUnprocessableEntity, "quota_model.ParseLimitSubject", err)
+ return
+ }
+ subjs[i] = subj
+ }
+ subjects = &subjs
+ }
+
+ rule, err := ctx.QuotaRule.Edit(ctx, form.Limit, subjects)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "quota_model.rule.Edit", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToQuotaRuleInfo(*rule, true))
+}
+
+// DeleteQuotaRule deletes a quota rule
+func DeleteQuotaRule(ctx *context.APIContext) {
+ // swagger:operation DELETE /admin/quota/rules/{quotarule} admin adminDEleteQuotaRule
+ // ---
+ // summary: Deletes a quota rule
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: quotarule
+ // in: path
+ // description: quota rule to delete
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ err := quota_model.DeleteRuleByName(ctx, ctx.QuotaRule.Name)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "quota_model.DeleteRuleByName", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/admin/repo.go b/routers/api/v1/admin/repo.go
new file mode 100644
index 0000000..c119d53
--- /dev/null
+++ b/routers/api/v1/admin/repo.go
@@ -0,0 +1,49 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/repo"
+ "code.gitea.io/gitea/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. This user 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)
+}
diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go
new file mode 100644
index 0000000..329242d
--- /dev/null
+++ b/routers/api/v1/admin/runners.go
@@ -0,0 +1,26 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "code.gitea.io/gitea/routers/api/v1/shared"
+ "code.gitea.io/gitea/services/context"
+)
+
+// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
+
+// GetRegistrationToken returns the token to register global runners
+func GetRegistrationToken(ctx *context.APIContext) {
+ // swagger:operation GET /admin/runners/registration-token admin adminGetRunnerRegistrationToken
+ // ---
+ // summary: Get an global actions runner registration token
+ // produces:
+ // - application/json
+ // parameters:
+ // responses:
+ // "200":
+ // "$ref": "#/responses/RegistrationToken"
+
+ shared.GetRegistrationToken(ctx, 0, 0)
+}
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
new file mode 100644
index 0000000..9ea210e
--- /dev/null
+++ b/routers/api/v1/admin/user.go
@@ -0,0 +1,509 @@
+// 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"
+
+ "code.gitea.io/gitea/models"
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/auth/password"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/user"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/convert"
+ "code.gitea.io/gitea/services/mailer"
+ user_service "code.gitea.io/gitea/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.Error(http.StatusUnprocessableEntity, "", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "auth.GetSourceByID", 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.Error(http.StatusBadRequest, "PasswordIsRequired", err)
+ return
+ }
+
+ if !password.IsComplexEnough(form.Password) {
+ err := errors.New("PasswordComplexity")
+ ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
+ return
+ }
+
+ if err := password.IsPwned(ctx, form.Password); err != nil {
+ if password.IsErrIsPwnedRequest(err) {
+ log.Error(err.Error())
+ }
+ ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
+ return
+ }
+ }
+
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: optional.Some(true),
+ IsRestricted: optional.FromPtr(form.Restricted),
+ }
+
+ if form.Visibility != "" {
+ visibility := api.VisibilityModes[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, 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.Error(http.StatusUnprocessableEntity, "", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateUser", 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 user to edit
+ // 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)
+
+ // If either LoginSource or LoginName is given, the other must be present too.
+ if form.SourceID != nil || form.LoginName != nil {
+ if form.SourceID == nil || form.LoginName == nil {
+ ctx.Error(http.StatusUnprocessableEntity, "LoginSourceAndLoginName", fmt.Errorf("source_id and login_name must be specified together"))
+ return
+ }
+ }
+
+ authOpts := &user_service.UpdateAuthOptions{
+ LoginSource: optional.FromPtr(form.SourceID),
+ LoginName: optional.FromPtr(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.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
+ case errors.Is(err, password.ErrComplexity):
+ ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
+ case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
+ ctx.Error(http.StatusBadRequest, "PasswordIsPwned", err)
+ default:
+ ctx.Error(http.StatusInternalServerError, "UpdateAuth", err)
+ }
+ return
+ }
+
+ if form.Email != nil {
+ if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
+ switch {
+ case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
+ ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
+ case user_model.IsErrEmailAlreadyUsed(err):
+ ctx.Error(http.StatusBadRequest, "EmailUsed", err)
+ default:
+ ctx.Error(http.StatusInternalServerError, "AddOrSetPrimaryEmailAddress", err)
+ }
+ return
+ }
+
+ if !user_model.IsEmailDomainAllowed(*form.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", *form.Email))
+ }
+ }
+
+ opts := &user_service.UpdateOptions{
+ FullName: optional.FromPtr(form.FullName),
+ Website: optional.FromPtr(form.Website),
+ Location: optional.FromPtr(form.Location),
+ Description: optional.FromPtr(form.Description),
+ Pronouns: optional.FromPtr(form.Pronouns),
+ IsActive: optional.FromPtr(form.Active),
+ IsAdmin: optional.FromPtr(form.Admin),
+ Visibility: optional.FromNonDefault(api.VisibilityModes[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 models.IsErrDeleteLastAdminUser(err) {
+ ctx.Error(http.StatusBadRequest, "LastAdmin", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateUser", 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 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.Error(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.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("you cannot delete yourself"))
+ return
+ }
+
+ if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
+ if models.IsErrUserOwnRepos(err) ||
+ models.IsErrUserHasOrgs(err) ||
+ models.IsErrUserOwnPackages(err) ||
+ models.IsErrDeleteLastAdminUser(err) {
+ ctx.Error(http.StatusUnprocessableEntity, "", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteUser", 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
+ // 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 user
+ // 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.ParamsInt64(":id")); err != nil {
+ if asymkey_model.IsErrKeyNotExist(err) {
+ ctx.NotFound()
+ } else if asymkey_model.IsErrKeyAccessDenied(err) {
+ ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteUserPublicKey", 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: user's login name to search for
+ // 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/UserList"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ listOptions := utils.GetListOptions(ctx)
+
+ users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
+ Actor: ctx.Doer,
+ Type: user_model.UserTypeIndividual,
+ LoginName: ctx.FormTrim("login_name"),
+ SourceID: ctx.FormInt64("source_id"),
+ OrderBy: db.SearchOrderByAlphabetically,
+ ListOptions: listOptions,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "SearchUsers", err)
+ return
+ }
+
+ results := make([]*api.User, len(users))
+ for i := range users {
+ results[i] = convert.ToUser(ctx, users[i], ctx.Doer)
+ }
+
+ ctx.SetLinkHeader(int(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: existing username of 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.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
+ return
+ }
+
+ oldName := ctx.ContextUser.Name
+ newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
+
+ // Check if user name has been changed
+ if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
+ switch {
+ case user_model.IsErrUserAlreadyExist(err):
+ ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
+ case db.IsErrNameReserved(err):
+ ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName))
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName))
+ case db.IsErrNameCharsNotAllowed(err):
+ ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName))
+ default:
+ ctx.ServerError("ChangeUserName", err)
+ }
+ return
+ }
+
+ log.Trace("User name changed: %s -> %s", oldName, newName)
+ ctx.Status(http.StatusNoContent)
+}