0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-18 15:47:37 +02:00

Merge 11a319bdbc7dd9cd19d14ae7f9b1d51ece3b7b1e into c68925152b1b6c8f92806cdbda9c4672dcc1608f

This commit is contained in:
Karthik Bhandary 2026-06-17 11:20:45 +00:00 committed by GitHub
commit fb91417e2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 209 additions and 2 deletions

View File

@ -3063,6 +3063,13 @@
"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.",
"admin.users.all_orgs_removed": "User has been removed from all organizations.",
"admin.users.no_orgs_to_remove": "User is not a member of any 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",

View File

@ -29,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"
)
@ -518,6 +519,82 @@ func DeleteUser(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/-/admin/users")
}
// RemoveUserFromOrg removes a user from an organization
func RemoveUserFromOrg(ctx *context.Context) {
u := prepareUserInfo(ctx)
if ctx.Written() {
return
}
orgID := ctx.PathParamInt64("orgid")
org, err := org_model.GetOrgByID(ctx, orgID)
if err != nil {
ctx.ServerError("GetOrgByID", err)
return
}
if err := org_service.RemoveOrgUser(ctx, org, u); err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
return
}
ctx.ServerError("RemoveOrgUser", err)
return
}
ctx.Flash.Success(ctx.Tr("admin.users.org_removed"))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
}
// RemoveUserFromAllOrgs removes a user from all organizations
func RemoveUserFromAllOrgs(ctx *context.Context) {
u := prepareUserInfo(ctx)
if ctx.Written() {
return
}
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
ListOptions: db.ListOptionsAll,
UserID: u.ID,
IncludeVisibility: structs.VisibleTypePrivate,
})
if err != nil {
ctx.ServerError("FindOrgs", err)
return
}
if len(orgs) == 0 {
ctx.Flash.Info(ctx.Tr("admin.users.no_orgs_to_remove"))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
return
}
removedCount := 0
for i := range orgs {
if err := org_service.RemoveOrgUser(ctx, orgs[i], u); err != nil {
if org_model.IsErrLastOrgOwner(err) {
log.Warn("Cannot remove user %s from org %s: last owner", u.Name, orgs[i].Name)
continue
}
log.Error("Failed to remove user %s from org %s: %v", u.Name, orgs[i].Name, err)
continue
}
removedCount++
}
if removedCount == 0 {
ctx.Flash.Error(ctx.Tr("admin.users.no_orgs_removed"))
} else 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.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
}
// AvatarPost response for change user's avatar request
func AvatarPost(ctx *context.Context) {
u := prepareUserInfo(ctx)

View File

@ -788,6 +788,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/{orgid}/remove", admin.RemoveUserFromOrg)
m.Post("/{userid}/orgs/remove-all", admin.RemoveUserFromAllOrgs)
})
m.Group("/badges", func() {
@ -863,8 +865,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/actions", func() {
m.Get("", misc.LocationRedirect("./actions/runners"))
addSettingsRunnersRoutes()
m.Post("/runners/bulk", shared_actions.RunnerBulkActionPost)
addSettingsVariablesRoutes()
m.Post("/runners/bulk", shared_actions.RunnerBulkActionPost)
})
}, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled))
// ***** END: Admin *****

View File

@ -30,10 +30,33 @@
</div>
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.organization"}} ({{ctx.Locale.Tr "admin.total" .OrgsTotal}})
{{if gt .OrgsTotal 0}}
<div class="ui right">
<button class="ui red tiny button show-modal" data-modal="#remove-all-orgs-modal">{{ctx.Locale.Tr "remove_all"}}</button>
</div>
{{end}}
</h4>
<div class="ui attached segment">
{{template "explore/user_list" .}}
{{template "admin/user/view_orgs" .}}
</div>
</div>
{{if gt .OrgsTotal 0}}
<div class="ui small modal" id="remove-all-orgs-modal">
<div class="header">
{{ctx.Locale.Tr "admin.users.remove_all_orgs_title"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "admin.users.remove_all_orgs_desc" .User.Name .OrgsTotal}}</p>
</div>
<div class="actions">
<div class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</div>
<form method="post" action="{{.Link}}/orgs/remove-all" style="display: inline;">
{{.CsrfTokenHtml}}
<button class="ui red button" type="submit">{{ctx.Locale.Tr "remove_all"}}</button>
</form>
</div>
</div>
{{end}}
{{template "admin/layout_footer" .}}

View File

@ -0,0 +1,33 @@
<div class="flex-divided-list items-with-main">
{{range .Users}}
<div class="item tw-items-center">
<div class="item-leading">
{{ctx.AvatarUtils.Avatar . 48}}
</div>
<div class="item-main">
<div class="item-title">
{{template "shared/user/name" .}}
{{if .Visibility.IsPrivate}}
<span class="ui basic tiny label">{{ctx.Locale.Tr "repo.desc.private"}}</span>
{{end}}
</div>
<div class="item-body">
{{if .Location}}
<span class="flex-text-inline">{{svg "octicon-location"}}{{.Location}}</span>
{{end}}
<span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateUtils.AbsoluteShort .CreatedUnix)}}</span>
</div>
</div>
<div class="item-trailing">
<form method="post" action="{{$.Link}}/orgs/{{.ID}}/remove">
{{$.CsrfTokenHtml}}
<button class="ui red tiny button" type="submit">{{ctx.Locale.Tr "remove"}}</button>
</form>
</div>
</div>
{{else}}
<div class="item">
{{ctx.Locale.Tr "search.no_results"}}
</div>
{{end}}
</div>

View File

@ -0,0 +1,65 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"gitea.dev/models/organization"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/tests"
"github.com/stretchr/testify/assert"
)
func TestAdminRemoveUserFromOrg(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// Admin user
session := loginUser(t, "user1")
// User to remove from org
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
// Verify user is in org
isMember, err := organization.IsOrganizationMember(t.Context(), org.ID, user.ID)
assert.NoError(t, err)
assert.True(t, isMember)
// Remove user from org
req := NewRequest(t, "POST", "/-/admin/users/4/orgs/3/remove")
session.MakeRequest(t, req, http.StatusSeeOther)
// Verify user is no longer in org
isMember, err = organization.IsOrganizationMember(t.Context(), org.ID, user.ID)
assert.NoError(t, err)
assert.False(t, isMember)
}
func TestAdminRemoveUserFromAllOrgs(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// Admin user
session := loginUser(t, "user1")
// User to remove from all orgs (user4 is not a last owner)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
// Get count of orgs user is in before removal
orgCount, err := organization.GetOrganizationCount(t.Context(), user)
assert.NoError(t, err)
assert.Positive(t, orgCount, "User should be in at least one org")
// Remove user from all orgs
req := NewRequest(t, "POST", "/-/admin/users/4/orgs/remove-all")
session.MakeRequest(t, req, http.StatusSeeOther)
// Verify user is no longer in any orgs
orgCountAfter, err := organization.GetOrganizationCount(t.Context(), user)
assert.NoError(t, err)
assert.Equal(t, int64(0), orgCountAfter, "User should not be in any orgs")
}