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:
commit
fb91417e2b
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 *****
|
||||
|
||||
@ -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" .}}
|
||||
|
||||
33
templates/admin/user/view_orgs.tmpl
Normal file
33
templates/admin/user/view_orgs.tmpl
Normal 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>
|
||||
65
tests/integration/admin_user_org_test.go
Normal file
65
tests/integration/admin_user_org_test.go
Normal 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")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user