diff --git a/models/organization/org_user.go b/models/organization/org_user.go index 1e623b03e16..88646b51b07 100644 --- a/models/organization/org_user.go +++ b/models/organization/org_user.go @@ -56,6 +56,13 @@ func GetOrganizationCount(ctx context.Context, u *user_model.User) (int64, error Count(new(OrgUser)) } +func GetUserOrganizations(ctx context.Context, uid int64) (ret []*Organization, err error) { + err = db.GetEngine(ctx).Select("`user`.*"). + Join("INNER", "org_user", "org_user.org_id = `user`.id"). + Where("org_user.uid=?", uid).Find(&ret) + return ret, err +} + // IsOrganizationOwner returns true if given user is in the owner team. func IsOrganizationOwner(ctx context.Context, orgID, uid int64) (bool, error) { ownerTeam, err := GetOwnerTeam(ctx, orgID) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index a887742a6e4..7753c6d71c0 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -136,6 +136,7 @@ "show_full_screen": "Show full screen", "download_logs": "Download logs", "confirm_delete_selected": "Confirm to delete all selected items?", + "confirm_remove": "Confirm to remove?", "name": "Name", "value": "Value", "readme": "Readme", @@ -3100,6 +3101,12 @@ "admin.users.purge_help": "Forcibly delete user and any repositories, organizations, and packages owned by the user. All comments will be deleted too.", "admin.users.still_own_packages": "This user still owns one or more packages. Delete these packages first.", "admin.users.deletion_success": "The user account has been deleted.", + "admin.users.org_removed": "User has been removed from the organization %s.", + "admin.users.all_orgs_removed": "User has been removed from all organizations.", + "admin.users.no_orgs_removed": "Failed to remove user from organizations (may be last owner).", + "admin.users.some_orgs_removed": "User removed from %d of %d organizations (some may require another owner first).", + "admin.users.remove_all_orgs_title": "Remove from All Organizations?", + "admin.users.remove_all_orgs_desc": "Are you sure you want to remove %s from all %d organizations? This action cannot be undone.", "admin.users.reset_2fa": "Reset 2FA", "admin.users.list_status_filter.menu_text": "Filter", "admin.users.list_status_filter.reset": "Reset", diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index f9ae5975278..839ff70fb56 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -21,7 +21,6 @@ import ( "gitea.dev/modules/log" "gitea.dev/modules/optional" "gitea.dev/modules/setting" - "gitea.dev/modules/structs" "gitea.dev/modules/templates" "gitea.dev/modules/web" "gitea.dev/routers/web/explore" @@ -30,6 +29,7 @@ import ( "gitea.dev/services/context" "gitea.dev/services/forms" "gitea.dev/services/mailer" + org_service "gitea.dev/services/org" user_service "gitea.dev/services/user" ) @@ -293,19 +293,12 @@ func ViewUser(ctx *context.Context) { ctx.Data["Emails"] = emails ctx.Data["EmailsTotal"] = len(emails) - orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{ - ListOptions: db.ListOptionsAll, - UserID: u.ID, - IncludeVisibility: structs.VisibleTypePrivate, - }) + ctx.Data["UserOrgs"], err = org_model.GetUserOrganizations(ctx, u.ID) if err != nil { ctx.ServerError("FindOrgs", err) return } - ctx.Data["Users"] = orgs // needed to be able to use explore/user_list template - ctx.Data["OrgsTotal"] = len(orgs) - ctx.HTML(http.StatusOK, tplUserView) } @@ -518,6 +511,65 @@ func DeleteUser(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/-/admin/users") } +func RemoveUserFromOrg(ctx *context.Context) { + u := prepareUserInfo(ctx) + if ctx.Written() { + return + } + + orgID := ctx.PathParamInt64("org_id") + org, err := org_model.GetOrgByID(ctx, orgID) + if err != nil { + ctx.ServerError("GetOrgByID", err) + return + } + + err = org_service.RemoveOrgUser(ctx, org, u) + if org_model.IsErrLastOrgOwner(err) { + ctx.Flash.Error(ctx.Tr("form.last_org_owner")) + ctx.JSONRedirect("") + return + } else if err != nil { + ctx.ServerError("RemoveOrgUser", err) + return + } + + ctx.Flash.Success(ctx.Tr("admin.users.org_removed", org.Name)) + ctx.JSONRedirect("") +} + +func RemoveUserFromAllOrgs(ctx *context.Context) { + u := prepareUserInfo(ctx) + if ctx.Written() { + return + } + + orgs, err := org_model.GetUserOrganizations(ctx, u.ID) + if err != nil { + ctx.ServerError("GetUserOrganizations", err) + return + } + + removedCount := 0 + for i := range orgs { + err = org_service.RemoveOrgUser(ctx, orgs[i], u) + if org_model.IsErrLastOrgOwner(err) { + continue + } else if err != nil { + log.Error("Failed to remove user %s from org %s: %v", u.Name, orgs[i].Name, err) + continue + } + removedCount++ + } + + if removedCount < len(orgs) { + ctx.Flash.Warning(ctx.Tr("admin.users.some_orgs_removed", removedCount, len(orgs))) + } else { + ctx.Flash.Success(ctx.Tr("admin.users.all_orgs_removed")) + } + ctx.JSONRedirect("") +} + // AvatarPost response for change user's avatar request func AvatarPost(ctx *context.Context) { u := prepareUserInfo(ctx) diff --git a/routers/web/web.go b/routers/web/web.go index 861df9d866a..7588b53092a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -818,6 +818,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Post("/{userid}/delete", admin.DeleteUser) m.Post("/{userid}/avatar", web.Bind[*forms.AvatarForm](), admin.AvatarPost) m.Post("/{userid}/avatar/delete", admin.DeleteAvatar) + m.Post("/{userid}/orgs/{org_id}/remove", admin.RemoveUserFromOrg) + m.Post("/{userid}/orgs/remove-all", admin.RemoveUserFromAllOrgs) }) m.Group("/badges", func() { diff --git a/templates/admin/user/view.tmpl b/templates/admin/user/view.tmpl index a86b59d23c1..63011e47c6d 100644 --- a/templates/admin/user/view.tmpl +++ b/templates/admin/user/view.tmpl @@ -3,12 +3,12 @@