]> source.dussan.org Git - gitea.git/commitdiff
Add option to purge users (#18064)
authorzeripath <art27@cantab.net>
Thu, 14 Jul 2022 07:22:09 +0000 (08:22 +0100)
committerGitHub <noreply@github.com>
Thu, 14 Jul 2022 07:22:09 +0000 (08:22 +0100)
Add the ability to purge users when deleting them.

Close #15588

Signed-off-by: Andrew Thornton <art27@cantab.net>
16 files changed:
cmd/admin.go
integrations/admin_user_test.go
integrations/integration_test.go
models/packages/package_version.go
models/project/project.go
models/repo.go
models/user.go
options/locale/locale_en-US.ini
routers/api/v1/admin/user.go
routers/web/admin/users.go
routers/web/user/setting/account.go
services/packages/container/cleanup.go
services/packages/packages.go
services/user/user.go
services/user/user_test.go
templates/admin/user/edit.tmpl

index 32f9a95a66581a0fe8fc3b49ff6b68d19a028c77..524cc305638800d8bcfc488aebca1c0a0f5eace5 100644 (file)
@@ -157,6 +157,10 @@ var (
                                Name:  "email,e",
                                Usage: "Email of the user to delete",
                        },
+                       cli.BoolFlag{
+                               Name:  "purge",
+                               Usage: "Purge user, all their repositories, organizations and comments",
+                       },
                },
                Action: runDeleteUser,
        }
@@ -675,7 +679,7 @@ func runDeleteUser(c *cli.Context) error {
                return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
        }
 
-       return user_service.DeleteUser(user)
+       return user_service.DeleteUser(ctx, user, c.Bool("purge"))
 }
 
 func runGenerateAccessToken(c *cli.Context) error {
index 59adac7ecc65b0db22832b37d2ff69212368cffb..a2020652b747ff784e4528f68136946d8ab5b40e 100644 (file)
@@ -76,7 +76,7 @@ func TestAdminDeleteUser(t *testing.T) {
        req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{
                "_csrf": csrf,
        })
-       session.MakeRequest(t, req, http.StatusOK)
+       session.MakeRequest(t, req, http.StatusSeeOther)
 
        assertUserDeleted(t, 8)
        unittest.CheckConsistencyFor(t, &user_model.User{})
index 8a43de7c45fa93dce0b95d64e5b4e30f1a898727..230f780175c9c8ab423b26788bbc5261fa27980c 100644 (file)
@@ -188,8 +188,13 @@ func initIntegrationTest() {
 
        switch {
        case setting.Database.UseMySQL:
-               db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/",
-                       setting.Database.User, setting.Database.Passwd, setting.Database.Host))
+               connType := "tcp"
+               if len(setting.Database.Host) > 0 && setting.Database.Host[0] == '/' { // looks like a unix socket
+                       connType = "unix"
+               }
+
+               db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@%s(%s)/",
+                       setting.Database.User, setting.Database.Passwd, connType, setting.Database.Host))
                defer db.Close()
                if err != nil {
                        log.Fatal("sql.Open: %v", err)
index 583f832e5eec8ae77adc56841c56e1f804fee874..83c2fdb67489f74109b258d3a417374c2ca08309 100644 (file)
@@ -107,7 +107,7 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType
                        ExactMatch: true,
                        Value:      version,
                },
-               IsInternal: isInternal,
+               IsInternal: util.OptionalBoolOf(isInternal),
                Paginator:  db.NewAbsoluteListOptions(0, 1),
        })
        if err != nil {
@@ -171,7 +171,7 @@ type PackageSearchOptions struct {
        Name            SearchValue       // only results with the specific name are found
        Version         SearchValue       // only results with the specific version are found
        Properties      map[string]string // only results are found which contain all listed version properties with the specific value
-       IsInternal      bool
+       IsInternal      util.OptionalBool
        HasFileWithName string            // only results are found which are associated with a file with the specific name
        HasFiles        util.OptionalBool // only results are found which have associated files
        Sort            string
@@ -179,7 +179,10 @@ type PackageSearchOptions struct {
 }
 
 func (opts *PackageSearchOptions) toConds() builder.Cond {
-       var cond builder.Cond = builder.Eq{"package_version.is_internal": opts.IsInternal}
+       cond := builder.NewCond()
+       if !opts.IsInternal.IsNone() {
+               cond = builder.Eq{"package_version.is_internal": opts.IsInternal.IsTrue()}
+       }
 
        if opts.OwnerID != 0 {
                cond = cond.And(builder.Eq{"package.owner_id": opts.OwnerID})
index 0aa37cc5c9071bed6a4161f1217004bb83c8b608..86a77947d88380315804abe192a09e897b39950b 100644 (file)
@@ -330,3 +330,40 @@ func DeleteProjectByIDCtx(ctx context.Context, id int64) error {
 
        return updateRepositoryProjectCount(ctx, p.RepoID)
 }
+
+func DeleteProjectByRepoIDCtx(ctx context.Context, repoID int64) error {
+       switch {
+       case setting.Database.UseSQLite3:
+               if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue WHERE project_issue.id IN (SELECT project_issue.id FROM project_issue INNER JOIN project WHERE project.id = project_issue.project_id AND project.repo_id = ?)", repoID); err != nil {
+                       return err
+               }
+               if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board WHERE project_board.id IN (SELECT project_board.id FROM project_board INNER JOIN project WHERE project.id = project_board.project_id AND project.repo_id = ?)", repoID); err != nil {
+                       return err
+               }
+               if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
+                       return err
+               }
+       case setting.Database.UsePostgreSQL:
+               if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue USING project WHERE project.id = project_issue.project_id AND project.repo_id = ? ", repoID); err != nil {
+                       return err
+               }
+               if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board USING project WHERE project.id = project_board.project_id AND project.repo_id = ? ", repoID); err != nil {
+                       return err
+               }
+               if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
+                       return err
+               }
+       default:
+               if _, err := db.GetEngine(ctx).Exec("DELETE project_issue FROM project_issue INNER JOIN project ON project.id = project_issue.project_id WHERE project.repo_id = ? ", repoID); err != nil {
+                       return err
+               }
+               if _, err := db.GetEngine(ctx).Exec("DELETE project_board FROM project_board INNER JOIN project ON project.id = project_board.project_id WHERE project.repo_id = ? ", repoID); err != nil {
+                       return err
+               }
+               if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
+                       return err
+               }
+       }
+
+       return updateRepositoryProjectCount(ctx, repoID)
+}
index ca83b03e42ca159726d15dc90fe4cd0068498ad8..66ef51473950f2bd61de6c5ab156d72eaf203b8b 100644 (file)
@@ -342,16 +342,8 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
                }
        }
 
-       projects, _, err := project_model.GetProjects(ctx, project_model.SearchOptions{
-               RepoID: repoID,
-       })
-       if err != nil {
-               return fmt.Errorf("get projects: %v", err)
-       }
-       for i := range projects {
-               if err := project_model.DeleteProjectByIDCtx(ctx, projects[i].ID); err != nil {
-                       return fmt.Errorf("delete project [%d]: %v", projects[i].ID, err)
-               }
+       if err := project_model.DeleteProjectByRepoIDCtx(ctx, repoID); err != nil {
+               return fmt.Errorf("unable to delete projects for repo[%d]: %v", repoID, err)
        }
 
        // Remove LFS objects
index 49374014aa7db864073a08992f6465026b7a90ae..86a714e746bb29efc8f44d22f11a3fe48778fe5e 100644 (file)
@@ -27,7 +27,7 @@ import (
 )
 
 // DeleteUser deletes models associated to an user.
-func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
+func DeleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) {
        e := db.GetEngine(ctx)
 
        // ***** START: Watch *****
@@ -95,8 +95,8 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
                return err
        }
 
-       if setting.Service.UserDeleteWithCommentsMaxTime != 0 &&
-               u.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now()) {
+       if purge || (setting.Service.UserDeleteWithCommentsMaxTime != 0 &&
+               u.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now())) {
 
                // Delete Comments
                const batchSize = 50
index 9e8a0303393b6800e2eec299817f425cb86dfeda..167d2cc1f20c633b7224cf835ab9c8bedc6b6a7a 100644 (file)
@@ -2540,6 +2540,8 @@ users.delete_account = Delete User Account
 users.cannot_delete_self = "You cannot delete yourself"
 users.still_own_repo = This user still owns one or more repositories. Delete or transfer these repositories first.
 users.still_has_org = This user is a member of an organization. Remove the user from any organizations first.
+users.purge = Purge User
+users.purge_help = Forcibly delete user and any repositories, organizations, and packages owned by the user. All comments will be deleted too.
 users.still_own_packages = This user still owns one or more packages. Delete these packages first.
 users.deletion_success = The user account has been deleted.
 users.reset_2fa = Reset 2FA
index 71932136b1f987b40e413041036a8aac9aea2a54..1a4b02001133dc2a467f7faa3bc8f20ab0a4421e 100644 (file)
@@ -316,7 +316,7 @@ func DeleteUser(ctx *context.APIContext) {
                return
        }
 
-       if err := user_service.DeleteUser(ctx.ContextUser); err != nil {
+       if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
                if models.IsErrUserOwnRepos(err) ||
                        models.IsErrUserHasOrgs(err) ||
                        models.IsErrUserOwnPackages(err) {
index c37ecfd71ea9044a9d3e028add3622afbb899f57..aab633ec84b24bff44f2bf14f832ad47ca4c3ce1 100644 (file)
@@ -419,29 +419,21 @@ func DeleteUser(ctx *context.Context) {
        // admin should not delete themself
        if u.ID == ctx.Doer.ID {
                ctx.Flash.Error(ctx.Tr("admin.users.cannot_delete_self"))
-               ctx.JSON(http.StatusOK, map[string]interface{}{
-                       "redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")),
-               })
+               ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
                return
        }
 
-       if err = user_service.DeleteUser(u); err != nil {
+       if err = user_service.DeleteUser(ctx, u, ctx.FormBool("purge")); err != nil {
                switch {
                case models.IsErrUserOwnRepos(err):
                        ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
-                       ctx.JSON(http.StatusOK, map[string]interface{}{
-                               "redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")),
-                       })
+                       ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
                case models.IsErrUserHasOrgs(err):
                        ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
-                       ctx.JSON(http.StatusOK, map[string]interface{}{
-                               "redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")),
-                       })
+                       ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
                case models.IsErrUserOwnPackages(err):
                        ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages"))
-                       ctx.JSON(http.StatusOK, map[string]interface{}{
-                               "redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
-                       })
+                       ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"))
                default:
                        ctx.ServerError("DeleteUser", err)
                }
@@ -450,9 +442,7 @@ func DeleteUser(ctx *context.Context) {
        log.Trace("Account deleted by admin (%s): %s", ctx.Doer.Name, u.Name)
 
        ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/admin/users",
-       })
+       ctx.Redirect(setting.AppSubURL + "/admin/users")
 }
 
 // AvatarPost response for change user's avatar request
index dfade13a1ce484b146ded9454ab89c3eaef07e89..cdb24c6066741cd1a0eb09608fca7f2adbfd7212 100644 (file)
@@ -248,7 +248,7 @@ func DeleteAccount(ctx *context.Context) {
                return
        }
 
-       if err := user.DeleteUser(ctx.Doer); err != nil {
+       if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil {
                switch {
                case models.IsErrUserOwnRepos(err):
                        ctx.Flash.Error(ctx.Tr("form.still_own_repo"))
index 390a0b7b052d546e12b3b6e82a9488f1d3e03a5e..3e44f9aa1a0f625756d4a9925f78a02f38cc81af 100644 (file)
@@ -59,7 +59,7 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
                        ExactMatch: true,
                        Value:      container_model.UploadVersion,
                },
-               IsInternal: true,
+               IsInternal: util.OptionalBoolTrue,
                HasFiles:   util.OptionalBoolFalse,
        })
        if err != nil {
index 7f25fce5b85cc437388b92493b60589daee613fc..0ebf6e7df0cd8b30fdfe61e40be93c50a160dd43 100644 (file)
@@ -13,6 +13,7 @@ import (
 
        "code.gitea.io/gitea/models/db"
        packages_model "code.gitea.io/gitea/models/packages"
+       repo_model "code.gitea.io/gitea/models/repo"
        user_model "code.gitea.io/gitea/models/user"
        "code.gitea.io/gitea/modules/json"
        "code.gitea.io/gitea/modules/log"
@@ -451,3 +452,30 @@ func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) (
        }
        return s, pf, err
 }
+
+// RemoveAllPackages for User
+func RemoveAllPackages(ctx context.Context, userID int64) (int, error) {
+       count := 0
+       for {
+               pkgVersions, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+                       Paginator: &db.ListOptions{
+                               PageSize: repo_model.RepositoryListDefaultPageSize,
+                               Page:     1,
+                       },
+                       OwnerID: userID,
+               })
+               if err != nil {
+                       return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err)
+               }
+               if len(pkgVersions) == 0 {
+                       break
+               }
+               for _, pv := range pkgVersions {
+                       if err := DeletePackageVersionAndReferences(ctx, pv); err != nil {
+                               return count, fmt.Errorf("unable to delete package %d:%s[%d]. Error: %w", pv.PackageID, pv.Version, pv.ID, err)
+                       }
+                       count++
+               }
+       }
+       return count, nil
+}
index 4db4d7ca17f12af3ac2c40c69e67750443c4e989..448b7c2daf8dc80d6e5ebd18ae72ddf4737465c3 100644 (file)
@@ -21,19 +21,116 @@ import (
        repo_model "code.gitea.io/gitea/models/repo"
        user_model "code.gitea.io/gitea/models/user"
        "code.gitea.io/gitea/modules/avatar"
+       "code.gitea.io/gitea/modules/eventsource"
        "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
        "code.gitea.io/gitea/modules/storage"
        "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/services/packages"
 )
 
 // DeleteUser completely and permanently deletes everything of a user,
 // but issues/comments/pulls will be kept and shown as someone has been deleted,
 // unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.
-func DeleteUser(u *user_model.User) error {
+func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
        if u.IsOrganization() {
                return fmt.Errorf("%s is an organization not a user", u.Name)
        }
 
+       if purge {
+               // Disable the user first
+               // NOTE: This is deliberately not within a transaction as it must disable the user immediately to prevent any further action by the user to be purged.
+               if err := user_model.UpdateUserCols(ctx, &user_model.User{
+                       ID:              u.ID,
+                       IsActive:        false,
+                       IsRestricted:    true,
+                       IsAdmin:         false,
+                       ProhibitLogin:   true,
+                       Passwd:          "",
+                       Salt:            "",
+                       PasswdHashAlgo:  "",
+                       MaxRepoCreation: 0,
+               }, "is_active", "is_restricted", "is_admin", "prohibit_login", "max_repo_creation", "passwd", "salt", "passwd_hash_algo"); err != nil {
+                       return fmt.Errorf("unable to disable user: %s[%d] prior to purge. UpdateUserCols: %w", u.Name, u.ID, err)
+               }
+
+               // Force any logged in sessions to log out
+               // FIXME: We also need to tell the session manager to log them out too.
+               eventsource.GetManager().SendMessage(u.ID, &eventsource.Event{
+                       Name: "logout",
+               })
+
+               // Delete all repos belonging to this user
+               // Now this is not within a transaction because there are internal transactions within the DeleteRepository
+               // BUT: the db will still be consistent even if a number of repos have already been deleted.
+               // And in fact we want to capture any repositories that are being created in other transactions in the meantime
+               //
+               // An alternative option here would be write a DeleteAllRepositoriesForUserID function which would delete all of the repos
+               // but such a function would likely get out of date
+               for {
+                       repos, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
+                               ListOptions: db.ListOptions{
+                                       PageSize: repo_model.RepositoryListDefaultPageSize,
+                                       Page:     1,
+                               },
+                               Private: true,
+                               OwnerID: u.ID,
+                       })
+                       if err != nil {
+                               return fmt.Errorf("SearchRepositoryByName: %v", err)
+                       }
+                       if len(repos) == 0 {
+                               break
+                       }
+                       for _, repo := range repos {
+                               if err := models.DeleteRepository(u, u.ID, repo.ID); err != nil {
+                                       return fmt.Errorf("unable to delete repository %s for %s[%d]. Error: %v", repo.Name, u.Name, u.ID, err)
+                               }
+                       }
+               }
+
+               // Remove from Organizations and delete last owner organizations
+               // Now this is not within a transaction because there are internal transactions within the DeleteOrganization
+               // BUT: the db will still be consistent even if a number of organizations memberships and organizations have already been deleted
+               // And in fact we want to capture any organization additions that are being created in other transactions in the meantime
+               //
+               // An alternative option here would be write a function which would delete all organizations but it seems
+               // but such a function would likely get out of date
+               for {
+                       orgs, err := organization.FindOrgs(organization.FindOrgOptions{
+                               ListOptions: db.ListOptions{
+                                       PageSize: repo_model.RepositoryListDefaultPageSize,
+                                       Page:     1,
+                               },
+                               UserID:         u.ID,
+                               IncludePrivate: true,
+                       })
+                       if err != nil {
+                               return fmt.Errorf("unable to find org list for %s[%d]. Error: %v", u.Name, u.ID, err)
+                       }
+                       if len(orgs) == 0 {
+                               break
+                       }
+                       for _, org := range orgs {
+                               if err := models.RemoveOrgUser(org.ID, u.ID); err != nil {
+                                       if organization.IsErrLastOrgOwner(err) {
+                                               err = organization.DeleteOrganization(ctx, org)
+                                       }
+                                       if err != nil {
+                                               return fmt.Errorf("unable to remove user %s[%d] from org %s[%d]. Error: %v", u.Name, u.ID, org.Name, org.ID, err)
+                                       }
+                               }
+                       }
+               }
+
+               // Delete Packages
+               if setting.Packages.Enabled {
+                       if _, err := packages.RemoveAllPackages(ctx, u.ID); err != nil {
+                               return err
+                       }
+               }
+       }
+
        ctx, committer, err := db.TxContext()
        if err != nil {
                return err
@@ -41,7 +138,8 @@ func DeleteUser(u *user_model.User) error {
        defer committer.Close()
 
        // Note: A user owns any repository or belongs to any organization
-       //      cannot perform delete operation.
+       //      cannot perform delete operation. This causes a race with the purge above
+       //  however consistency requires that we ensure that this is the case
 
        // Check ownership of repository.
        count, err := repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{OwnerID: u.ID})
@@ -66,7 +164,7 @@ func DeleteUser(u *user_model.User) error {
                return models.ErrUserOwnPackages{UID: u.ID}
        }
 
-       if err := models.DeleteUser(ctx, u); err != nil {
+       if err := models.DeleteUser(ctx, u, purge); err != nil {
                return fmt.Errorf("DeleteUser: %v", err)
        }
 
@@ -117,7 +215,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
                        return db.ErrCancelledf("Before delete inactive user %s", u.Name)
                default:
                }
-               if err := DeleteUser(u); err != nil {
+               if err := DeleteUser(ctx, u, false); err != nil {
                        // Ignore users that were set inactive by admin.
                        if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
                                continue
index cfa02b003311620b342d89e8a41ec833493cf679..aefbcd9ecb491e78798e1fd2e780c592349c0df6 100644 (file)
@@ -33,7 +33,7 @@ func TestDeleteUser(t *testing.T) {
                ownedRepos := make([]*repo_model.Repository, 0, 10)
                assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&ownedRepos, &repo_model.Repository{OwnerID: userID}))
                if len(ownedRepos) > 0 {
-                       err := DeleteUser(user)
+                       err := DeleteUser(db.DefaultContext, user, false)
                        assert.Error(t, err)
                        assert.True(t, models.IsErrUserOwnRepos(err))
                        return
@@ -47,7 +47,7 @@ func TestDeleteUser(t *testing.T) {
                                return
                        }
                }
-               assert.NoError(t, DeleteUser(user))
+               assert.NoError(t, DeleteUser(db.DefaultContext, user, false))
                unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
                unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{})
        }
@@ -57,7 +57,7 @@ func TestDeleteUser(t *testing.T) {
        test(11)
 
        org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
-       assert.Error(t, DeleteUser(org))
+       assert.Error(t, DeleteUser(db.DefaultContext, org, false))
 }
 
 func TestCreateUser(t *testing.T) {
@@ -72,7 +72,7 @@ func TestCreateUser(t *testing.T) {
 
        assert.NoError(t, user_model.CreateUser(user))
 
-       assert.NoError(t, DeleteUser(user))
+       assert.NoError(t, DeleteUser(db.DefaultContext, user, false))
 }
 
 func TestCreateUser_Issue5882(t *testing.T) {
@@ -101,6 +101,6 @@ func TestCreateUser_Issue5882(t *testing.T) {
 
                assert.Equal(t, !u.AllowCreateOrganization, v.disableOrgCreation)
 
-               assert.NoError(t, DeleteUser(v.user))
+               assert.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
        }
 }
index 29dcaa127af28c90f80614ea91ee7ef8fc07ec4e..17cbef9f108a7eab162e3ad2d725aab56f39f653 100644 (file)
 
                                <div class="field">
                                        <button class="ui green button">{{.locale.Tr "admin.users.update_profile"}}</button>
-                                       <div class="ui red button delete-button" data-url="{{$.Link}}/delete" data-id="{{.User.ID}}">{{.locale.Tr "admin.users.delete_account"}}</div>
+                                       <div class="ui red button show-modal" data-modal="#delete-user-modal" data-url="{{$.Link}}/delete" data-id="{{.User.ID}}">{{.locale.Tr "admin.users.delete_account"}}</div>
                                </div>
                        </form>
                </div>
        </div>
 </div>
 
-<div class="ui small basic delete modal">
+<div class="ui small basic delete modal" id="delete-user-modal">
        <div class="ui icon header">
                {{svg "octicon-trash"}}
                {{.locale.Tr "settings.delete_account_title"}}
        <div class="content">
                <p>{{.locale.Tr "settings.delete_account_desc"}}</p>
        </div>
-       {{template "base/delete_modal_actions" .}}
+       <form class="ui form" method="POST" action="{{.Link}}/delete">
+               {{$.CsrfTokenHtml}}
+               <input type="hidden" name="id">
+               <div class="field">
+                       <div class="ui checkbox">
+                               <label for="purge">{{.locale.Tr "admin.users.purge"}}</label>
+                               <input name="purge" type="checkbox">
+                       </div>
+                       <p class="help">{{.locale.Tr "admin.users.purge_help"}}</p>
+               </div>
+               {{template "base/delete_modal_actions" .}}
+       </form>
 </div>
 {{template "base/footer" .}}