feat: add ability for admins to mark account as bot

This commit is contained in:
Maxim Slipenko 2026-05-14 18:09:34 +03:00 committed by Maxim Slipenko
parent 26f18a94ee
commit fc296cdd8d
11 changed files with 41 additions and 14 deletions

View file

@ -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}
}

View file

@ -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",

View file

@ -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 {

View file

@ -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)

View file

@ -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() {

View file

@ -47,6 +47,7 @@ type AdminEditUserForm struct {
Active bool
Admin bool
Restricted bool
Bot bool
AllowGitHook bool
AllowImportLocal bool
AllowCreateOrganization bool

View file

@ -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...)
}

View file

@ -135,6 +135,13 @@
</div>
<span class="help tw-block">{{ctx.Locale.Tr "admin.users.admin.description"}}</span>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "admin.users.is_bot"}}</label>
<input name="bot" type="checkbox" {{if .User.IsBot}}checked{{end}}>
</div>
<span class="help tw-block">{{ctx.Locale.Tr "admin.users.bot.description"}}</span>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "admin.users.is_restricted"}}</label>

View file

@ -9,6 +9,9 @@
{{if .User.IsAdmin}}
<span class="ui basic label">{{ctx.Locale.Tr "admin.users.admin"}}</span>
{{end}}
{{if .User.IsBot}}
<span class="ui basic label">{{ctx.Locale.Tr "admin.users.bot"}}</span>
{{end}}
</div>
<div class="flex-item-body">
<b>{{ctx.Locale.Tr "admin.users.auth_source"}}:</b>

View file

@ -3,5 +3,5 @@
data-tooltip-content="{{ctx.Locale.Tr "user.ghost.tooltip"}}">Ghost</a>
{{else}}
<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>
{{if .IsBot}}<span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}
{{if .IsBot}}<span class="ui basic label tw-p-1 tw-align-baseline">{{ctx.Locale.Tr "admin.users.bot"}}</span>{{end}}
{{end}}

View file

@ -1,6 +1,6 @@
<overflow-menu class="ui secondary pointing tabular borderless menu">
<div class="overflow-menu-items">
{{if and .HasProfileReadme .ContextUser.IsIndividual}}
{{if and .HasProfileReadme .ContextUser.IsUser}}
<a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview">
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
</a>
@ -12,7 +12,7 @@
{{end}}
<span hidden test-name="repository-count">{{.RepoCount}}</span>
</a>
{{if or .ContextUser.IsIndividual .CanReadProjects}}
{{if or .ContextUser.IsUser .CanReadProjects}}
<a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item">
{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
{{if .ProjectCount}}
@ -21,7 +21,7 @@
<span hidden test-name="project-count">{{.ProjectCount}}</span>
</a>
{{end}}
{{if and .IsPackageEnabled (or .ContextUser.IsIndividual .CanReadPackages)}}
{{if and .IsPackageEnabled (or .ContextUser.IsUser .CanReadPackages)}}
<a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item">
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
{{if .PackageCount}}
@ -30,12 +30,12 @@
<span hidden test-name="package-count">{{.PackageCount}}</span>
</a>
{{end}}
{{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual .CanReadCode)}}
{{if and .IsRepoIndexerEnabled (or .ContextUser.IsUser .CanReadCode)}}
<a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item">
{{svg "octicon-code"}} {{ctx.Locale.Tr "user.code"}}
</a>
{{end}}
{{if .ContextUser.IsIndividual}}
{{if .ContextUser.IsUser}}
{{if or (eq .TabName "activity") .IsAdmin (eq .SignedUserID .ContextUser.ID) (not .ContextUser.KeepActivityPrivate)}}
<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}