Skip to content

Commit cb52b17

Browse files
Add admin API route for managing user's badges (#23106)
Fix #22785 --------- Co-authored-by: Lunny Xiao <[email protected]>
1 parent e71eb89 commit cb52b17

File tree

9 files changed

+523
-2
lines changed

9 files changed

+523
-2
lines changed

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,8 @@ var migrations = []Migration{
558558
NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun),
559559
// v286 -> v287
560560
NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
561+
// v287 -> v288
562+
NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
561563
}
562564

563565
// GetCurrentDBVersion returns the current db version

models/migrations/v1_22/v287.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_22 //nolint
5+
6+
import (
7+
"xorm.io/xorm"
8+
)
9+
10+
type BadgeUnique struct {
11+
ID int64 `xorm:"pk autoincr"`
12+
Slug string `xorm:"UNIQUE"`
13+
}
14+
15+
func (BadgeUnique) TableName() string {
16+
return "badge"
17+
}
18+
19+
func UseSlugInsteadOfIDForBadges(x *xorm.Engine) error {
20+
type Badge struct {
21+
Slug string
22+
}
23+
24+
err := x.Sync(new(Badge))
25+
if err != nil {
26+
return err
27+
}
28+
29+
sess := x.NewSession()
30+
defer sess.Close()
31+
if err := sess.Begin(); err != nil {
32+
return err
33+
}
34+
35+
_, err = sess.Exec("UPDATE `badge` SET `slug` = `id` Where `slug` IS NULL")
36+
if err != nil {
37+
return err
38+
}
39+
40+
err = sess.Sync(new(BadgeUnique))
41+
if err != nil {
42+
return err
43+
}
44+
45+
return sess.Commit()
46+
}

models/migrations/v1_22/v287_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_22 //nolint
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"code.gitea.io/gitea/models/migrations/base"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func Test_UpdateBadgeColName(t *testing.T) {
16+
type Badge struct {
17+
ID int64 `xorm:"pk autoincr"`
18+
Description string
19+
ImageURL string
20+
}
21+
22+
// Prepare and load the testing database
23+
x, deferable := base.PrepareTestEnv(t, 0, new(BadgeUnique), new(Badge))
24+
defer deferable()
25+
if x == nil || t.Failed() {
26+
return
27+
}
28+
29+
oldBadges := []Badge{
30+
{ID: 1, Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"},
31+
{ID: 2, Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"},
32+
{ID: 3, Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"},
33+
}
34+
35+
for _, badge := range oldBadges {
36+
_, err := x.Insert(&badge)
37+
assert.NoError(t, err)
38+
}
39+
40+
if err := UseSlugInsteadOfIDForBadges(x); err != nil {
41+
assert.NoError(t, err)
42+
return
43+
}
44+
45+
got := []BadgeUnique{}
46+
if err := x.Table("badge").Asc("id").Find(&got); !assert.NoError(t, err) {
47+
return
48+
}
49+
50+
for i, e := range oldBadges {
51+
got := got[i]
52+
assert.Equal(t, e.ID, got.ID)
53+
assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug)
54+
}
55+
56+
// TODO: check if badges have been updated
57+
}

models/user/badge.go

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ package user
55

66
import (
77
"context"
8+
"fmt"
89

910
"code.gitea.io/gitea/models/db"
1011
)
1112

1213
// Badge represents a user badge
1314
type Badge struct {
14-
ID int64 `xorm:"pk autoincr"`
15+
ID int64 `xorm:"pk autoincr"`
16+
Slug string `xorm:"UNIQUE"`
1517
Description string
1618
ImageURL string
1719
}
@@ -39,3 +41,84 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
3941
count, err := sess.FindAndCount(&badges)
4042
return badges, count, err
4143
}
44+
45+
// CreateBadge creates a new badge.
46+
func CreateBadge(ctx context.Context, badge *Badge) error {
47+
_, err := db.GetEngine(ctx).Insert(badge)
48+
return err
49+
}
50+
51+
// GetBadge returns a badge
52+
func GetBadge(ctx context.Context, slug string) (*Badge, error) {
53+
badge := new(Badge)
54+
has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
55+
if !has {
56+
return nil, err
57+
}
58+
return badge, err
59+
}
60+
61+
// UpdateBadge updates a badge based on its slug.
62+
func UpdateBadge(ctx context.Context, badge *Badge) error {
63+
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
64+
return err
65+
}
66+
67+
// DeleteBadge deletes a badge.
68+
func DeleteBadge(ctx context.Context, badge *Badge) error {
69+
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge)
70+
return err
71+
}
72+
73+
// AddUserBadge adds a badge to a user.
74+
func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
75+
return AddUserBadges(ctx, u, []*Badge{badge})
76+
}
77+
78+
// AddUserBadges adds badges to a user.
79+
func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
80+
return db.WithTx(ctx, func(ctx context.Context) error {
81+
for _, badge := range badges {
82+
// hydrate badge and check if it exists
83+
has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge)
84+
if err != nil {
85+
return err
86+
} else if !has {
87+
return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
88+
}
89+
if err := db.Insert(ctx, &UserBadge{
90+
BadgeID: badge.ID,
91+
UserID: u.ID,
92+
}); err != nil {
93+
return err
94+
}
95+
}
96+
return nil
97+
})
98+
}
99+
100+
// RemoveUserBadge removes a badge from a user.
101+
func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
102+
return RemoveUserBadges(ctx, u, []*Badge{badge})
103+
}
104+
105+
// RemoveUserBadges removes badges from a user.
106+
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
107+
return db.WithTx(ctx, func(ctx context.Context) error {
108+
for _, badge := range badges {
109+
if _, err := db.GetEngine(ctx).
110+
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
111+
Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
112+
Delete(&UserBadge{}); err != nil {
113+
return err
114+
}
115+
}
116+
return nil
117+
})
118+
}
119+
120+
// RemoveAllUserBadges removes all badges from a user.
121+
func RemoveAllUserBadges(ctx context.Context, u *User) error {
122+
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
123+
return err
124+
}

modules/structs/user.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright 2014 The Gogs Authors. All rights reserved.
2+
// Copyright 2023 The Gitea Authors. All rights reserved.
23
// SPDX-License-Identifier: MIT
34

45
package structs
@@ -108,3 +109,33 @@ type UpdateUserAvatarOption struct {
108109
// image must be base64 encoded
109110
Image string `json:"image" binding:"Required"`
110111
}
112+
113+
// Badge represents a user badge
114+
// swagger:model
115+
type Badge struct {
116+
ID int64 `json:"id"`
117+
Slug string `json:"slug"`
118+
Description string `json:"description"`
119+
ImageURL string `json:"image_url"`
120+
}
121+
122+
// UserBadge represents a user badge
123+
// swagger:model
124+
type UserBadge struct {
125+
ID int64 `json:"id"`
126+
BadgeID int64 `json:"badge_id"`
127+
UserID int64 `json:"user_id"`
128+
}
129+
130+
// UserBadgeOption options for link between users and badges
131+
type UserBadgeOption struct {
132+
// example: ["badge1","badge2"]
133+
BadgeSlugs []string `json:"badge_slugs" binding:"Required"`
134+
}
135+
136+
// BadgeList
137+
// swagger:response BadgeList
138+
type BadgeList struct {
139+
// in:body
140+
Body []Badge `json:"body"`
141+
}

routers/api/v1/admin/user_badge.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package admin
5+
6+
import (
7+
"net/http"
8+
9+
user_model "code.gitea.io/gitea/models/user"
10+
api "code.gitea.io/gitea/modules/structs"
11+
"code.gitea.io/gitea/modules/web"
12+
"code.gitea.io/gitea/services/context"
13+
)
14+
15+
// ListUserBadges lists all badges belonging to a user
16+
func ListUserBadges(ctx *context.APIContext) {
17+
// swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges
18+
// ---
19+
// summary: List a user's badges
20+
// produces:
21+
// - application/json
22+
// parameters:
23+
// - name: username
24+
// in: path
25+
// description: username of user
26+
// type: string
27+
// required: true
28+
// responses:
29+
// "200":
30+
// "$ref": "#/responses/BadgeList"
31+
// "404":
32+
// "$ref": "#/responses/notFound"
33+
34+
badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
35+
if err != nil {
36+
ctx.Error(http.StatusInternalServerError, "GetUserBadges", err)
37+
return
38+
}
39+
40+
ctx.SetTotalCountHeader(maxResults)
41+
ctx.JSON(http.StatusOK, &badges)
42+
}
43+
44+
// AddUserBadges add badges to a user
45+
func AddUserBadges(ctx *context.APIContext) {
46+
// swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges
47+
// ---
48+
// summary: Add a badge to a user
49+
// consumes:
50+
// - application/json
51+
// produces:
52+
// - application/json
53+
// parameters:
54+
// - name: username
55+
// in: path
56+
// description: username of user
57+
// type: string
58+
// required: true
59+
// - name: body
60+
// in: body
61+
// schema:
62+
// "$ref": "#/definitions/UserBadgeOption"
63+
// responses:
64+
// "204":
65+
// "$ref": "#/responses/empty"
66+
// "403":
67+
// "$ref": "#/responses/forbidden"
68+
69+
form := web.GetForm(ctx).(*api.UserBadgeOption)
70+
badges := prepareBadgesForReplaceOrAdd(ctx, *form)
71+
72+
if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil {
73+
ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
74+
return
75+
}
76+
77+
ctx.Status(http.StatusNoContent)
78+
}
79+
80+
// DeleteUserBadges delete a badge from a user
81+
func DeleteUserBadges(ctx *context.APIContext) {
82+
// swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges
83+
// ---
84+
// summary: Remove a badge from a user
85+
// produces:
86+
// - application/json
87+
// parameters:
88+
// - name: username
89+
// in: path
90+
// description: username of user
91+
// type: string
92+
// required: true
93+
// - name: body
94+
// in: body
95+
// schema:
96+
// "$ref": "#/definitions/UserBadgeOption"
97+
// responses:
98+
// "204":
99+
// "$ref": "#/responses/empty"
100+
// "403":
101+
// "$ref": "#/responses/forbidden"
102+
// "422":
103+
// "$ref": "#/responses/validationError"
104+
105+
form := web.GetForm(ctx).(*api.UserBadgeOption)
106+
badges := prepareBadgesForReplaceOrAdd(ctx, *form)
107+
108+
if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil {
109+
ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
110+
return
111+
}
112+
113+
ctx.Status(http.StatusNoContent)
114+
}
115+
116+
func prepareBadgesForReplaceOrAdd(ctx *context.APIContext, form api.UserBadgeOption) []*user_model.Badge {
117+
badges := make([]*user_model.Badge, len(form.BadgeSlugs))
118+
for i, badge := range form.BadgeSlugs {
119+
badges[i] = &user_model.Badge{
120+
Slug: badge,
121+
}
122+
}
123+
return badges
124+
}

routers/api/v1/api.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,6 +1519,9 @@ func Routes() *web.Route {
15191519
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
15201520
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
15211521
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
1522+
m.Get("/badges", admin.ListUserBadges)
1523+
m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges)
1524+
m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges)
15221525
}, context.UserAssignmentAPI())
15231526
})
15241527
m.Group("/emails", func() {

0 commit comments

Comments
 (0)