// Copyright 2026 The Gitea Authors. // SPDX-License-Identifier: MIT package admin import ( "errors" "net/http" "net/url" "strings" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" ) const ( tplBadges templates.TplName = "admin/badge/list" tplBadgeNew templates.TplName = "admin/badge/new" tplBadgeView templates.TplName = "admin/badge/view" tplBadgeEdit templates.TplName = "admin/badge/edit" tplBadgeUsers templates.TplName = "admin/badge/users" ) // BadgeSearchDefaultAdminSort is the default sort type for admin view const BadgeSearchDefaultAdminSort = "oldest" // Badges show all the badges func Badges(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.badges") ctx.Data["PageIsAdminBadges"] = true RenderBadgeSearch(ctx, &user_model.SearchBadgeOptions{ ListOptions: db.ListOptions{ Page: max(ctx.FormInt("page"), 1), PageSize: setting.UI.Admin.UserPagingNum, }, }, tplBadges) } // NewBadge render adding a new badge func NewBadge(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.badges.new_badge") ctx.Data["PageIsAdminBadges"] = true ctx.HTML(http.StatusOK, tplBadgeNew) } // NewBadgePost response for adding a new badge func NewBadgePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.AdminCreateBadgeForm) if ctx.HasError() { ctx.JSONError(ctx.GetErrMsg()) return } b := &user_model.Badge{ Slug: form.Slug, Description: form.Description, ImageURL: form.ImageURL, } if err := user_model.CreateBadge(ctx, b); err != nil { if errors.Is(err, util.ErrAlreadyExist) { ctx.JSONError(ctx.Tr("admin.badges.slug_been_taken")) } else { ctx.ServerError("CreateBadge", err) } return } log.Trace("Badge created by admin (%s): %s", ctx.Doer.Name, b.Slug) ctx.Flash.Success(ctx.Tr("admin.badges.new_success", b.Slug)) ctx.JSONRedirect(setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(b.Slug)) } func prepareBadgeInfo(ctx *context.Context) *user_model.Badge { b, err := user_model.GetBadge(ctx, ctx.PathParam("badge_slug")) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Redirect(setting.AppSubURL + "/-/admin/badges") } else { ctx.ServerError("GetBadge", err) } return nil } ctx.Data["Badge"] = b return b } func ViewBadge(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.badges.details") ctx.Data["PageIsAdminBadges"] = true prepareBadgeInfo(ctx) if ctx.Written() { return } badge := ctx.Data["Badge"].(*user_model.Badge) opts := &user_model.GetBadgeUsersOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: setting.UI.Admin.UserPagingNum, }, BadgeSlug: badge.Slug, } users, count, err := user_model.GetBadgeUsers(ctx, opts) if err != nil { ctx.ServerError("GetBadgeUsers", err) return } ctx.Data["Users"] = users ctx.Data["UsersTotal"] = int(count) ctx.HTML(http.StatusOK, tplBadgeView) } // EditBadge show editing badge page func EditBadge(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.badges.edit_badge") ctx.Data["PageIsAdminBadges"] = true prepareBadgeInfo(ctx) if ctx.Written() { return } ctx.HTML(http.StatusOK, tplBadgeEdit) } // EditBadgePost response for editing badge func EditBadgePost(ctx *context.Context) { b := prepareBadgeInfo(ctx) if ctx.Written() { return } form := web.GetForm(ctx).(*forms.AdminEditBadgeForm) if ctx.HasError() { ctx.JSONError(ctx.GetErrMsg()) return } b.ImageURL = form.ImageURL b.Description = form.Description if err := user_model.UpdateBadge(ctx, b); err != nil { ctx.ServerError("UpdateBadge", err) return } log.Trace("Badge updated by admin (%s): %s", ctx.Doer.Name, b.Slug) ctx.Flash.Success(ctx.Tr("admin.badges.update_success")) ctx.JSONRedirect(setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(ctx.PathParam("badge_slug"))) } // DeleteBadge response for deleting a badge func DeleteBadge(ctx *context.Context) { b, err := user_model.GetBadge(ctx, ctx.PathParam("badge_slug")) if err != nil { ctx.ServerError("GetBadge", err) return } if err = user_model.DeleteBadge(ctx, b); err != nil { ctx.ServerError("DeleteBadge", err) return } log.Trace("Badge deleted by admin (%s): %s", ctx.Doer.Name, b.Slug) ctx.Flash.Success(ctx.Tr("admin.badges.deletion_success")) ctx.Redirect(setting.AppSubURL + "/-/admin/badges") } func BadgeUsers(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.badges.users_with_badge", ctx.PathParam("badge_slug")) ctx.Data["PageIsAdminBadges"] = true page := max(ctx.FormInt("page"), 1) badge := &user_model.Badge{Slug: ctx.PathParam("badge_slug")} opts := &user_model.GetBadgeUsersOptions{ ListOptions: db.ListOptions{ Page: page, PageSize: setting.UI.Admin.UserPagingNum, }, BadgeSlug: badge.Slug, } users, count, err := user_model.GetBadgeUsers(ctx, opts) if err != nil { ctx.ServerError("GetBadgeUsers", err) return } ctx.Data["Users"] = users ctx.Data["Total"] = count ctx.Data["Page"] = context.NewPagination(count, setting.UI.Admin.UserPagingNum, page, 5) ctx.HTML(http.StatusOK, tplBadgeUsers) } // BadgeUsersPost response for actions for user badges func BadgeUsersPost(ctx *context.Context) { name := strings.ToLower(ctx.FormString("user")) u, err := user_model.GetUserByName(ctx, name) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Flash.Error(ctx.Tr("form.user_not_exist")) ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) } else { ctx.ServerError("GetUserByName", err) } return } if err = user_model.AddUserBadge(ctx, u, &user_model.Badge{Slug: ctx.PathParam("badge_slug")}); err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Flash.Error(ctx.Tr("admin.badges.not_found")) ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) } else if errors.Is(err, util.ErrAlreadyExist) { ctx.Flash.Error(ctx.Tr("admin.badges.user_already_has")) ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) } else { ctx.ServerError("AddUserBadge", err) } return } ctx.Flash.Success(ctx.Tr("admin.badges.user_add_success")) ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) } // DeleteBadgeUser delete a badge from a user func DeleteBadgeUser(ctx *context.Context) { badgeUsersURL := setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(ctx.PathParam("badge_slug")) + "/users" user, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Flash.Error(ctx.Tr("form.user_not_exist")) ctx.JSONRedirect(badgeUsersURL) return } else { ctx.ServerError("GetUserByID", err) return } } if err := user_model.RemoveUserBadge(ctx, user, &user_model.Badge{Slug: ctx.PathParam("badge_slug")}); err == nil { ctx.Flash.Success(ctx.Tr("admin.badges.user_remove_success")) } else { ctx.ServerError("RemoveUserBadge", err) return } ctx.JSONRedirect(badgeUsersURL) } func RenderBadgeSearch(ctx *context.Context, opts *user_model.SearchBadgeOptions, tplName templates.TplName) { var ( badges []*user_model.Badge count int64 err error orderBy db.SearchOrderBy ) sortOrder := ctx.FormString("sort") if sortOrder == "" { sortOrder = BadgeSearchDefaultAdminSort } ctx.Data["SortType"] = sortOrder switch sortOrder { case "newest": orderBy = "`badge`.id DESC" case "oldest": orderBy = "`badge`.id ASC" case "reversealphabetically": orderBy = "`badge`.slug DESC" case "alphabetically": orderBy = "`badge`.slug ASC" default: // In case the sort type is invalid, keep admin default sorting. ctx.Data["SortType"] = "oldest" orderBy = "`badge`.id ASC" } opts.Keyword = ctx.FormTrim("q") opts.OrderBy = orderBy if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) { badges, count, err = user_model.SearchBadges(ctx, opts) if err != nil { ctx.ServerError("SearchBadges", err) return } } ctx.Data["Keyword"] = opts.Keyword ctx.Data["Total"] = count ctx.Data["Badges"] = badges pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplName) }