From 689fb82a7043fdb2fee02195701b0bc728e99709 Mon Sep 17 00:00:00 2001 From: Bruno Sofiato Date: Fri, 31 Jan 2025 21:59:49 -0300 Subject: [PATCH] Inclusion of rename organization api (#33303) This adds an endpoint (`/orgs/{org}/rename`) to rename organizations. I've modeled the endpoint using the rename user endpoint -- `/admin/users/{username}/rename` -- as base. It is the 1st time I wrote a new API endpoint (I've tried to follow the rename users endpoint code while writing it). So feel free to ping me if there is something wrong or missing. Resolves #32995 --------- Signed-off-by: Bruno Sofiato Co-authored-by: delvh Co-authored-by: wxiaoguang (cherry picked from commit 040c830dec5c727a56d16df62b1673bce6fca645) Conflicts: routers/api/v1/admin/user.go ignore this unrelated change templates/swagger/v1_json.tmpl regenerate tests/integration/api_org_test.go port as a standalone test instead of refactoring the tests --- modules/structs/org.go | 9 +++++ routers/api/v1/api.go | 1 + routers/api/v1/org/org.go | 38 +++++++++++++++++++++ routers/api/v1/swagger/options.go | 3 ++ templates/swagger/v1_json.tmpl | 56 +++++++++++++++++++++++++++++++ tests/integration/api_org_test.go | 23 +++++++++++++ 6 files changed, 130 insertions(+) diff --git a/modules/structs/org.go b/modules/structs/org.go index 686345b2c3..451153b620 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -57,3 +57,12 @@ type EditOrgOption struct { Visibility string `json:"visibility" binding:"In(,public,limited,private)"` RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` } + +// RenameOrgOption options when renaming an organization +type RenameOrgOption struct { + // New username for this org. This name cannot be in use yet by any other user. + // + // required: true + // unique: true + NewName string `json:"new_name" binding:"Required"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8e1ccdc5e2..684686b1e2 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1505,6 +1505,7 @@ func Routes() *web.Route { m.Combo("").Get(org.Get). Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). Delete(reqToken(), reqOrgOwnership(), org.Delete) + m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename) m.Combo("/repos").Get(user.ListOrgRepos). Post(reqToken(), bind(api.CreateRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetOrg), repo.CreateOrgRepo) m.Group("/members", func() { diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 6759360def..7d503c3ad7 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -320,6 +320,44 @@ func Get(ctx *context.APIContext) { ctx.JSON(http.StatusOK, org) } +func Rename(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/rename organization renameOrg + // --- + // summary: Rename an organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: existing org name + // type: string + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/RenameOrgOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.RenameOrgOption) + orgUser := ctx.Org.Organization.AsUser() + if err := user_service.RenameUser(ctx, orgUser, form.NewName); err != nil { + if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { + ctx.Error(http.StatusUnprocessableEntity, "RenameOrg", err) + } else { + ctx.ServerError("RenameOrg", err) + } + return + } + ctx.Status(http.StatusNoContent) +} + // Edit change an organization's information func Edit(ctx *context.APIContext) { // swagger:operation PATCH /orgs/{org} organization orgEdit diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 432e42d4e7..48c11c467f 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -216,6 +216,9 @@ type swaggerParameterBodies struct { // in:body CreateVariableOption api.CreateVariableOption + // in:body + RenameOrgOption api.RenameOrgOption + // in:body UpdateVariableOption api.UpdateVariableOption diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 1832e9d732..2a8252557e 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3772,6 +3772,46 @@ } } }, + "/orgs/{org}/rename": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Rename an organization", + "operationId": "renameOrg", + "parameters": [ + { + "type": "string", + "description": "existing org name", + "name": "org", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RenameOrgOption" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/orgs/{org}/repos": { "get": { "produces": [ @@ -26634,6 +26674,22 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "RenameOrgOption": { + "description": "RenameOrgOption options when renaming an organization", + "type": "object", + "required": [ + "new_name" + ], + "properties": { + "new_name": { + "description": "New username for this org. This name cannot be in use yet by any other user.", + "type": "string", + "uniqueItems": true, + "x-go-name": "NewName" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "RenameUserOption": { "description": "RenameUserOption options when renaming a user", "type": "object", diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index c26cf196de..5f92271d64 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -99,6 +99,29 @@ func TestAPIOrgCreate(t *testing.T) { assert.EqualValues(t, "user1", users[0].UserName) } +func TestAPIOrgRename(t *testing.T) { + defer tests.PrepareTestEnv(t)() + token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization) + + org := api.CreateOrgOption{ + UserName: "user1_org", + FullName: "User1's organization", + Description: "This organization created by user1", + Website: "https://try.gitea.io", + Location: "Shanghai", + Visibility: "limited", + } + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/user1_org/rename", &api.RenameOrgOption{ + NewName: "renamed_org", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + unittest.AssertExistsAndLoadBean(t, &org_model.Organization{Name: "renamed_org"}) +} + func TestAPIOrgEdit(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user1")