diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0d11674aa9971..7621b2f54f149 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -711,7 +711,9 @@ func Routes(ctx gocontext.Context) *web.Route { }, reqToken()) m.Group("/user", func() { - m.Get("", user.GetAuthenticatedUser) + m.Combo(""). + Get(user.GetAuthenticatedUser). + Delete(reqBasicOrRevProxyAuth(), user.DeleteAuthenticatedUser) m.Group("/settings", func() { m.Get("", user.GetUserSettings) m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings) diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 69197aef23eb2..6eddec2f8aeaf 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -6,13 +6,17 @@ package user import ( + "fmt" "net/http" + "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/routers/api/v1/utils" + user_service "code.gitea.io/gitea/services/user" ) // Search search users @@ -146,3 +150,46 @@ func GetUserHeatmapData(ctx *context.APIContext) { } ctx.JSON(http.StatusOK, heatmap) } + +// DeleteAuthenticatedUser deletes the current user +func DeleteAuthenticatedUser(ctx *context.APIContext) { + // swagger:operation DELETE /user user userDeleteCurrent + // --- + // summary: Delete the authenticated user + // produces: + // - application/json + // parameters: + // - name: purge + // in: query + // description: Purge user, all their repositories, organizations and comments + // type: boolean + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" + + // Extract out the username as it's unavailable after deleting the user. + username := ctx.Doer.Name + + if ctx.Doer.IsOrganization() { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", username)) + return + } + + if err := user_service.DeleteUser(ctx, ctx.Doer, ctx.FormBool("purge")); err != nil { + if models.IsErrUserOwnRepos(err) || + models.IsErrUserHasOrgs(err) || + models.IsErrUserOwnPackages(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { + ctx.Error(http.StatusInternalServerError, "DeleteAuthenticatedUser", err) + } + return + } + log.Trace("Account deleted: %s", username) + + ctx.Status(http.StatusNoContent) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 728e88b734aa5..92eafc9ab53af 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -11760,6 +11760,35 @@ "$ref": "#/responses/User" } } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete the authenticated user", + "operationId": "userDeleteCurrent", + "parameters": [ + { + "type": "boolean", + "description": "Purge user, all their repositories, organizations and comments", + "name": "purge", + "in": "query" + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" + } + } } }, "/user/applications/oauth2": { diff --git a/tests/integration/api_user_delete_test.go b/tests/integration/api_user_delete_test.go new file mode 100644 index 0000000000000..3df1e8027e8e0 --- /dev/null +++ b/tests/integration/api_user_delete_test.go @@ -0,0 +1,52 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integration + +import ( + "net/http" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/tests" +) + +func TestAPIDeleteUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // 1 -> Admin + // 8 -> Normal user + for _, userID := range []int64{1, 8} { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) + t.Logf("Testing username %s", user.Name) + + req := NewRequest(t, "DELETE", "/api/v1/user") + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusNoContent) + + assertUserDeleted(t, userID) + unittest.CheckConsistencyFor(t, &user_model.User{}) + } +} + +func TestAPIPurgeUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + + // Cannot delete the user as it still has ownership of repositories + req := NewRequest(t, "DELETE", "/api/v1/user") + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusUnprocessableEntity) + + unittest.CheckConsistencyFor(t, &user_model.User{ID: 5}) + + req = NewRequest(t, "DELETE", "/api/v1/user?purge=true") + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusNoContent) + + assertUserDeleted(t, 5) + unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{}) +}