Skip to content

Commit 507b0b1

Browse files
authored
Merge pull request #127 from DeliciousBuding/rebase-onto-v2.1.3
重构文档中心、账号分组与 API Key 权限管理
2 parents 71d0ee5 + ab123ef commit 507b0b1

43 files changed

Lines changed: 14407 additions & 3697 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

admin/account_groups.go

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
package admin
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"net/http"
8+
"strconv"
9+
"strings"
10+
"time"
11+
"unicode/utf8"
12+
13+
"github.com/codex2api/database"
14+
"github.com/gin-gonic/gin"
15+
)
16+
17+
const (
18+
maxAccountGroups = 64
19+
maxAccountGroupNameRuneSize = 80
20+
)
21+
22+
type accountGroupResponse struct {
23+
ID int64 `json:"id"`
24+
Name string `json:"name"`
25+
Description string `json:"description"`
26+
Color string `json:"color"`
27+
SortOrder int64 `json:"sort_order"`
28+
MemberCount int64 `json:"member_count"`
29+
CreatedAt string `json:"created_at"`
30+
UpdatedAt string `json:"updated_at"`
31+
}
32+
33+
func toAccountGroupResponse(g database.AccountGroup) accountGroupResponse {
34+
return accountGroupResponse{
35+
ID: g.ID,
36+
Name: g.Name,
37+
Description: g.Description,
38+
Color: g.Color,
39+
SortOrder: g.SortOrder,
40+
MemberCount: g.MemberCount,
41+
CreatedAt: g.CreatedAt.Format(time.RFC3339),
42+
UpdatedAt: g.UpdatedAt.Format(time.RFC3339),
43+
}
44+
}
45+
46+
func (h *Handler) ListAccountGroups(c *gin.Context) {
47+
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
48+
defer cancel()
49+
groups, err := h.db.ListAccountGroups(ctx)
50+
if err != nil {
51+
writeInternalError(c, err)
52+
return
53+
}
54+
out := make([]accountGroupResponse, 0, len(groups))
55+
for _, group := range groups {
56+
out = append(out, toAccountGroupResponse(group))
57+
}
58+
c.JSON(http.StatusOK, gin.H{"groups": out})
59+
}
60+
61+
type createAccountGroupReq struct {
62+
Name string `json:"name"`
63+
Description string `json:"description"`
64+
Color string `json:"color"`
65+
SortOrder *int64 `json:"sort_order"`
66+
}
67+
68+
func (h *Handler) CreateAccountGroup(c *gin.Context) {
69+
var req createAccountGroupReq
70+
if err := c.ShouldBindJSON(&req); err != nil {
71+
writeError(c, http.StatusBadRequest, "请求格式错误")
72+
return
73+
}
74+
name, err := sanitizeAccountGroupName(req.Name)
75+
if err != nil {
76+
writeError(c, http.StatusBadRequest, err.Error())
77+
return
78+
}
79+
description := strings.TrimSpace(req.Description)
80+
if utf8.RuneCountInString(description) > 240 {
81+
writeError(c, http.StatusBadRequest, "描述长度不能超过 240 字符")
82+
return
83+
}
84+
color := strings.TrimSpace(req.Color)
85+
if utf8.RuneCountInString(color) > 20 {
86+
writeError(c, http.StatusBadRequest, "颜色长度不能超过 20 字符")
87+
return
88+
}
89+
sortOrder := int64(0)
90+
if req.SortOrder != nil {
91+
sortOrder = *req.SortOrder
92+
}
93+
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
94+
defer cancel()
95+
groups, err := h.db.ListAccountGroups(ctx)
96+
if err != nil {
97+
writeInternalError(c, err)
98+
return
99+
}
100+
if len(groups) >= maxAccountGroups {
101+
writeError(c, http.StatusBadRequest, "分组数量已达上限")
102+
return
103+
}
104+
id, err := h.db.CreateAccountGroup(ctx, name, description, color, sortOrder)
105+
if err != nil {
106+
if errors.Is(err, database.ErrDuplicateAccountGroupName) {
107+
writeError(c, http.StatusConflict, err.Error())
108+
return
109+
}
110+
writeInternalError(c, err)
111+
return
112+
}
113+
c.JSON(http.StatusOK, gin.H{"id": id, "message": "分组已创建"})
114+
}
115+
116+
type updateAccountGroupReq struct {
117+
Name *string `json:"name"`
118+
Description *string `json:"description"`
119+
Color *string `json:"color"`
120+
SortOrder *int64 `json:"sort_order"`
121+
}
122+
123+
func (h *Handler) UpdateAccountGroup(c *gin.Context) {
124+
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
125+
if err != nil {
126+
writeError(c, http.StatusBadRequest, "无效的分组 ID")
127+
return
128+
}
129+
var req updateAccountGroupReq
130+
if err := c.ShouldBindJSON(&req); err != nil {
131+
writeError(c, http.StatusBadRequest, "请求格式错误")
132+
return
133+
}
134+
if req.Name != nil {
135+
name, err := sanitizeAccountGroupName(*req.Name)
136+
if err != nil {
137+
writeError(c, http.StatusBadRequest, err.Error())
138+
return
139+
}
140+
req.Name = &name
141+
}
142+
if req.Description != nil {
143+
desc := strings.TrimSpace(*req.Description)
144+
if utf8.RuneCountInString(desc) > 240 {
145+
writeError(c, http.StatusBadRequest, "描述长度不能超过 240 字符")
146+
return
147+
}
148+
req.Description = &desc
149+
}
150+
if req.Color != nil {
151+
color := strings.TrimSpace(*req.Color)
152+
if utf8.RuneCountInString(color) > 20 {
153+
writeError(c, http.StatusBadRequest, "颜色长度不能超过 20 字符")
154+
return
155+
}
156+
req.Color = &color
157+
}
158+
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
159+
defer cancel()
160+
if err := h.db.UpdateAccountGroup(ctx, id, req.Name, req.Description, req.Color, req.SortOrder); err != nil {
161+
if errors.Is(err, sql.ErrNoRows) {
162+
writeError(c, http.StatusNotFound, "分组不存在")
163+
return
164+
}
165+
if errors.Is(err, database.ErrDuplicateAccountGroupName) {
166+
writeError(c, http.StatusConflict, err.Error())
167+
return
168+
}
169+
writeInternalError(c, err)
170+
return
171+
}
172+
writeMessage(c, http.StatusOK, "分组已更新")
173+
}
174+
175+
func (h *Handler) DeleteAccountGroup(c *gin.Context) {
176+
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
177+
if err != nil {
178+
writeError(c, http.StatusBadRequest, "无效的分组 ID")
179+
return
180+
}
181+
force := strings.EqualFold(c.Query("force"), "true")
182+
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
183+
defer cancel()
184+
if err := h.db.DeleteAccountGroup(ctx, id, force); err != nil {
185+
if errors.Is(err, sql.ErrNoRows) {
186+
writeError(c, http.StatusNotFound, "分组不存在")
187+
return
188+
}
189+
if errors.Is(err, database.ErrAccountGroupNotEmpty) {
190+
writeError(c, http.StatusConflict, err.Error())
191+
return
192+
}
193+
writeInternalError(c, err)
194+
return
195+
}
196+
if h.store != nil {
197+
for _, acc := range h.store.Accounts() {
198+
acc.Mu().RLock()
199+
groups := removeInt64(acc.GroupIDs, id)
200+
acc.Mu().RUnlock()
201+
h.store.ApplyAccountGroups(acc.DBID, groups)
202+
}
203+
}
204+
h.refreshAPIKeyAllowedGroupsAfterGroupDelete(ctx, id)
205+
writeMessage(c, http.StatusOK, "分组已删除")
206+
}
207+
208+
func (h *Handler) refreshAPIKeyAllowedGroupsAfterGroupDelete(ctx context.Context, groupID int64) {
209+
if h == nil || h.db == nil || groupID <= 0 {
210+
return
211+
}
212+
keys, err := h.db.ListAPIKeys(ctx)
213+
if err != nil {
214+
return
215+
}
216+
for _, key := range keys {
217+
if key == nil {
218+
continue
219+
}
220+
if h.store != nil {
221+
h.store.SetAPIKeyAllowedGroups(key.ID, key.AllowedGroupIDs)
222+
}
223+
h.invalidateAPIKeyRuntimeCaches(ctx, key.Key)
224+
}
225+
}
226+
227+
func sanitizeAccountGroupName(raw string) (string, error) {
228+
name := strings.TrimSpace(raw)
229+
if name == "" {
230+
return "", errors.New("分组名称不能为空")
231+
}
232+
if utf8.RuneCountInString(name) > maxAccountGroupNameRuneSize {
233+
return "", errors.New("分组名称长度超过 80 字符")
234+
}
235+
for _, r := range name {
236+
if r < 0x20 || r == 0x7f {
237+
return "", errors.New("分组名称包含非法控制字符")
238+
}
239+
}
240+
return name, nil
241+
}
242+
243+
func removeInt64(slice []int64, target int64) []int64 {
244+
out := make([]int64, 0, len(slice))
245+
for _, v := range slice {
246+
if v != target {
247+
out = append(out, v)
248+
}
249+
}
250+
return out
251+
}
252+
253+
func containsInt64(slice []int64, target int64) bool {
254+
for _, v := range slice {
255+
if v == target {
256+
return true
257+
}
258+
}
259+
return false
260+
}
261+
262+
func dedupeInt64(ids []int64) []int64 {
263+
if len(ids) == 0 {
264+
return nil
265+
}
266+
seen := make(map[int64]struct{}, len(ids))
267+
out := make([]int64, 0, len(ids))
268+
for _, id := range ids {
269+
if id <= 0 {
270+
continue
271+
}
272+
if _, ok := seen[id]; ok {
273+
continue
274+
}
275+
seen[id] = struct{}{}
276+
out = append(out, id)
277+
}
278+
return out
279+
}

0 commit comments

Comments
 (0)