From fc296cdd8d98857759dbc0ee5a64a44d16b51906 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Thu, 14 May 2026 18:09:34 +0300 Subject: [PATCH] feat: add ability for admins to mark account as bot --- models/user/search.go | 2 +- options/locale_next/locale_en-US.json | 2 ++ routers/web/admin/users.go | 1 + routers/web/user/profile.go | 2 +- routers/web/web.go | 10 +++++----- services/forms/admin.go | 1 + services/user/update.go | 15 ++++++++++++++- templates/admin/user/edit.tmpl | 7 +++++++ templates/admin/user/view_details.tmpl | 3 +++ templates/shared/user/authorlink.tmpl | 2 +- templates/user/overview/header.tmpl | 10 +++++----- 11 files changed, 41 insertions(+), 14 deletions(-) diff --git a/models/user/search.go b/models/user/search.go index 54a1ac0dfe..204f928af6 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -48,7 +48,7 @@ type SearchUserOptions struct { func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session { var cond builder.Cond if opts.Type == UserTypeIndividual { - cond = builder.In("type", UserTypeIndividual, UserTypeRemoteUser) + cond = builder.In("type", UserTypeIndividual, UserTypeBot, UserTypeRemoteUser) } else { cond = builder.Eq{"type": opts.Type} } diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json index f2ae4c0943..dcc084f679 100644 --- a/options/locale_next/locale_en-US.json +++ b/options/locale_next/locale_en-US.json @@ -222,6 +222,8 @@ "admin.users.list_status_filter.not_prohibit_login": "Allow login", "admin.users.list_status_filter.is_2fa_enabled": "2FA enabled", "admin.users.list_status_filter.not_2fa_enabled": "2FA disabled", + "admin.users.is_bot": "Bot account", + "admin.users.bot.description": "Mark the account as a bot.", "admin.monitor.queues": "Queues", "admin.monitor.queue": "Queue: %s", "admin.monitor.queue.name": "Name", diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index d4a32c03a0..18c0f41d98 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -455,6 +455,7 @@ func EditUserPost(ctx *context.Context) { Visibility: optional.Some(form.Visibility), Language: optional.Some(form.Language), KeepEmailPrivate: optional.Some(form.HideEmail), + IsBot: optional.Some(form.Bot), } if err := user_service.UpdateUser(ctx, u, opts); err != nil { diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 1c5392ba83..60c11da97c 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -421,7 +421,7 @@ func Action(ctx *context.Context) { } } - if ctx.ContextUser.IsIndividual() { + if ctx.ContextUser.IsUser() { shared_user.PrepareContextForProfileBigAvatar(ctx) ctx.Data["IsHTMX"] = true ctx.HTML(http.StatusOK, tplProfileBigAvatar) diff --git a/routers/web/web.go b/routers/web/web.go index ba9ad277db..eabc4d0a04 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -943,9 +943,9 @@ func registerRoutes(m *web.Route) { reqRepoProjectsReader(ctx) } - individualPermsChecker := func(ctx *context.Context) { - // org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked. - if ctx.ContextUser.IsIndividual() { + userPermsChecker := func(ctx *context.Context) { + // org permissions have been checked in context.OrgAssignment(), but user permissions haven't been checked. + if ctx.ContextUser.IsUser() { switch ctx.ContextUser.Visibility { case structs.VisibleTypePrivate: if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) { @@ -1148,11 +1148,11 @@ func registerRoutes(m *web.Route) { return } }) - }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true), individualPermsChecker) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true), userPermsChecker) m.Group("", func() { m.Get("/code", user.CodeSearch) - }, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker) + }, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), userPermsChecker) }, ignSignIn, context.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code) m.Group("/{username}/{reponame}", func() { diff --git a/services/forms/admin.go b/services/forms/admin.go index dc2cc9c909..329a28d670 100644 --- a/services/forms/admin.go +++ b/services/forms/admin.go @@ -47,6 +47,7 @@ type AdminEditUserForm struct { Active bool Admin bool Restricted bool + Bot bool AllowGitHook bool AllowImportLocal bool AllowCreateOrganization bool diff --git a/services/user/update.go b/services/user/update.go index 8b2b9cce07..3608d34dce 100644 --- a/services/user/update.go +++ b/services/user/update.go @@ -5,6 +5,7 @@ package user import ( "context" + "errors" "fmt" "forgejo.org/models" @@ -41,10 +42,11 @@ type UpdateOptions struct { RepoAdminChangeTeamAccess optional.Option[bool] EnableRepoUnitHints optional.Option[bool] KeepPronounsPrivate optional.Option[bool] + IsBot optional.Option[bool] } func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) error { - cols := make([]string, 0, 20) + cols := make([]string, 0, 21) if has, value := opts.KeepEmailPrivate.Get(); has { u.KeepEmailPrivate = value @@ -144,6 +146,17 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er u.SetLastLogin() cols = append(cols, "last_login_unix") } + if has, value := opts.IsBot.Get(); has { + if !u.IsUser() { + return errors.New("changing to bot account for non user account is not allowed") + } + if value { + u.Type = user_model.UserTypeBot + } else { + u.Type = user_model.UserTypeIndividual + } + cols = append(cols, "type") + } return user_model.UpdateUserCols(ctx, u, cols...) } diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index f18317e694..6e95b97cd5 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -135,6 +135,13 @@ {{ctx.Locale.Tr "admin.users.admin.description"}} +
+
+ + +
+ {{ctx.Locale.Tr "admin.users.bot.description"}} +
diff --git a/templates/admin/user/view_details.tmpl b/templates/admin/user/view_details.tmpl index c394b4bd3d..3f190fbe47 100644 --- a/templates/admin/user/view_details.tmpl +++ b/templates/admin/user/view_details.tmpl @@ -9,6 +9,9 @@ {{if .User.IsAdmin}} {{ctx.Locale.Tr "admin.users.admin"}} {{end}} + {{if .User.IsBot}} + {{ctx.Locale.Tr "admin.users.bot"}} + {{end}}
{{ctx.Locale.Tr "admin.users.auth_source"}}: diff --git a/templates/shared/user/authorlink.tmpl b/templates/shared/user/authorlink.tmpl index 5be8a1612f..098aae1cf9 100644 --- a/templates/shared/user/authorlink.tmpl +++ b/templates/shared/user/authorlink.tmpl @@ -3,5 +3,5 @@ data-tooltip-content="{{ctx.Locale.Tr "user.ghost.tooltip"}}">Ghost {{else}} {{.GetDisplayName}} - {{if .IsBot}}bot{{end}} + {{if .IsBot}}{{ctx.Locale.Tr "admin.users.bot"}}{{end}} {{end}} diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl index cc306cc571..0e3a3f51cf 100644 --- a/templates/user/overview/header.tmpl +++ b/templates/user/overview/header.tmpl @@ -1,6 +1,6 @@
- {{if and .HasProfileReadme .ContextUser.IsIndividual}} + {{if and .HasProfileReadme .ContextUser.IsUser}} {{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}} @@ -12,7 +12,7 @@ {{end}} - {{if or .ContextUser.IsIndividual .CanReadProjects}} + {{if or .ContextUser.IsUser .CanReadProjects}} {{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}} {{if .ProjectCount}} @@ -21,7 +21,7 @@ {{end}} - {{if and .IsPackageEnabled (or .ContextUser.IsIndividual .CanReadPackages)}} + {{if and .IsPackageEnabled (or .ContextUser.IsUser .CanReadPackages)}} {{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} {{if .PackageCount}} @@ -30,12 +30,12 @@ {{end}} - {{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual .CanReadCode)}} + {{if and .IsRepoIndexerEnabled (or .ContextUser.IsUser .CanReadCode)}} {{svg "octicon-code"}} {{ctx.Locale.Tr "user.code"}} {{end}} - {{if .ContextUser.IsIndividual}} + {{if .ContextUser.IsUser}} {{if or (eq .TabName "activity") .IsAdmin (eq .SignedUserID .ContextUser.ID) (not .ContextUser.KeepActivityPrivate)}} {{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}