diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /routers/api/v1/admin | |
parent | Initial commit. (diff) | |
download | forgejo-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.go | 180 | ||||
-rw-r--r-- | routers/api/v1/admin/cron.go | 86 | ||||
-rw-r--r-- | routers/api/v1/admin/email.go | 87 | ||||
-rw-r--r-- | routers/api/v1/admin/hooks.go | 176 | ||||
-rw-r--r-- | routers/api/v1/admin/org.go | 123 | ||||
-rw-r--r-- | routers/api/v1/admin/quota.go | 53 | ||||
-rw-r--r-- | routers/api/v1/admin/quota_group.go | 436 | ||||
-rw-r--r-- | routers/api/v1/admin/quota_rule.go | 219 | ||||
-rw-r--r-- | routers/api/v1/admin/repo.go | 49 | ||||
-rw-r--r-- | routers/api/v1/admin/runners.go | 26 | ||||
-rw-r--r-- | routers/api/v1/admin/user.go | 509 |
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) +} |