Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
e7bc6250
Unverified
Commit
e7bc6250
authored
Jan 19, 2026
by
Wesley Liddick
Committed by
GitHub
Jan 19, 2026
Browse files
Merge pull request #333 from whoismonay/main
fix: 普通用户接口移除管理员敏感字段透传
parents
eb5e6214
c8fb9ef3
Changes
26
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/group_handler.go
View file @
e7bc6250
...
...
@@ -94,9 +94,9 @@ func (h *GroupHandler) List(c *gin.Context) {
return
}
outGroups
:=
make
([]
dto
.
Group
,
0
,
len
(
groups
))
outGroups
:=
make
([]
dto
.
Admin
Group
,
0
,
len
(
groups
))
for
i
:=
range
groups
{
outGroups
=
append
(
outGroups
,
*
dto
.
GroupFromService
(
&
groups
[
i
]))
outGroups
=
append
(
outGroups
,
*
dto
.
GroupFromService
Admin
(
&
groups
[
i
]))
}
response
.
Paginated
(
c
,
outGroups
,
total
,
page
,
pageSize
)
}
...
...
@@ -120,9 +120,9 @@ func (h *GroupHandler) GetAll(c *gin.Context) {
return
}
outGroups
:=
make
([]
dto
.
Group
,
0
,
len
(
groups
))
outGroups
:=
make
([]
dto
.
Admin
Group
,
0
,
len
(
groups
))
for
i
:=
range
groups
{
outGroups
=
append
(
outGroups
,
*
dto
.
GroupFromService
(
&
groups
[
i
]))
outGroups
=
append
(
outGroups
,
*
dto
.
GroupFromService
Admin
(
&
groups
[
i
]))
}
response
.
Success
(
c
,
outGroups
)
}
...
...
@@ -142,7 +142,7 @@ func (h *GroupHandler) GetByID(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
GroupFromService
(
group
))
response
.
Success
(
c
,
dto
.
GroupFromService
Admin
(
group
))
}
// Create handles creating a new group
...
...
@@ -177,7 +177,7 @@ func (h *GroupHandler) Create(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
GroupFromService
(
group
))
response
.
Success
(
c
,
dto
.
GroupFromService
Admin
(
group
))
}
// Update handles updating a group
...
...
@@ -219,7 +219,7 @@ func (h *GroupHandler) Update(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
GroupFromService
(
group
))
response
.
Success
(
c
,
dto
.
GroupFromService
Admin
(
group
))
}
// Delete handles deleting a group
...
...
backend/internal/handler/admin/redeem_handler.go
View file @
e7bc6250
...
...
@@ -54,9 +54,9 @@ func (h *RedeemHandler) List(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
RedeemCode
,
0
,
len
(
codes
))
out
:=
make
([]
dto
.
Admin
RedeemCode
,
0
,
len
(
codes
))
for
i
:=
range
codes
{
out
=
append
(
out
,
*
dto
.
RedeemCodeFromService
(
&
codes
[
i
]))
out
=
append
(
out
,
*
dto
.
RedeemCodeFromService
Admin
(
&
codes
[
i
]))
}
response
.
Paginated
(
c
,
out
,
total
,
page
,
pageSize
)
}
...
...
@@ -76,7 +76,7 @@ func (h *RedeemHandler) GetByID(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
RedeemCodeFromService
(
code
))
response
.
Success
(
c
,
dto
.
RedeemCodeFromService
Admin
(
code
))
}
// Generate handles generating new redeem codes
...
...
@@ -100,9 +100,9 @@ func (h *RedeemHandler) Generate(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
RedeemCode
,
0
,
len
(
codes
))
out
:=
make
([]
dto
.
Admin
RedeemCode
,
0
,
len
(
codes
))
for
i
:=
range
codes
{
out
=
append
(
out
,
*
dto
.
RedeemCodeFromService
(
&
codes
[
i
]))
out
=
append
(
out
,
*
dto
.
RedeemCodeFromService
Admin
(
&
codes
[
i
]))
}
response
.
Success
(
c
,
out
)
}
...
...
@@ -163,7 +163,7 @@ func (h *RedeemHandler) Expire(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
RedeemCodeFromService
(
code
))
response
.
Success
(
c
,
dto
.
RedeemCodeFromService
Admin
(
code
))
}
// GetStats handles getting redeem code statistics
...
...
backend/internal/handler/admin/subscription_handler.go
View file @
e7bc6250
...
...
@@ -83,9 +83,9 @@ func (h *SubscriptionHandler) List(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
UserSubscription
,
0
,
len
(
subscriptions
))
out
:=
make
([]
dto
.
Admin
UserSubscription
,
0
,
len
(
subscriptions
))
for
i
:=
range
subscriptions
{
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
(
&
subscriptions
[
i
]))
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
Admin
(
&
subscriptions
[
i
]))
}
response
.
PaginatedWithResult
(
c
,
out
,
toResponsePagination
(
pagination
))
}
...
...
@@ -105,7 +105,7 @@ func (h *SubscriptionHandler) GetByID(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
(
subscription
))
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
Admin
(
subscription
))
}
// GetProgress handles getting subscription usage progress
...
...
@@ -150,7 +150,7 @@ func (h *SubscriptionHandler) Assign(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
(
subscription
))
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
Admin
(
subscription
))
}
// BulkAssign handles bulk assigning subscriptions to multiple users
...
...
@@ -201,7 +201,7 @@ func (h *SubscriptionHandler) Extend(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
(
subscription
))
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
Admin
(
subscription
))
}
// Revoke handles revoking a subscription
...
...
@@ -239,9 +239,9 @@ func (h *SubscriptionHandler) ListByGroup(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
UserSubscription
,
0
,
len
(
subscriptions
))
out
:=
make
([]
dto
.
Admin
UserSubscription
,
0
,
len
(
subscriptions
))
for
i
:=
range
subscriptions
{
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
(
&
subscriptions
[
i
]))
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
Admin
(
&
subscriptions
[
i
]))
}
response
.
PaginatedWithResult
(
c
,
out
,
toResponsePagination
(
pagination
))
}
...
...
@@ -261,9 +261,9 @@ func (h *SubscriptionHandler) ListByUser(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
UserSubscription
,
0
,
len
(
subscriptions
))
out
:=
make
([]
dto
.
Admin
UserSubscription
,
0
,
len
(
subscriptions
))
for
i
:=
range
subscriptions
{
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
(
&
subscriptions
[
i
]))
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
Admin
(
&
subscriptions
[
i
]))
}
response
.
Success
(
c
,
out
)
}
...
...
backend/internal/handler/admin/usage_handler.go
View file @
e7bc6250
...
...
@@ -163,7 +163,7 @@ func (h *UsageHandler) List(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
UsageLog
,
0
,
len
(
records
))
out
:=
make
([]
dto
.
Admin
UsageLog
,
0
,
len
(
records
))
for
i
:=
range
records
{
out
=
append
(
out
,
*
dto
.
UsageLogFromServiceAdmin
(
&
records
[
i
]))
}
...
...
backend/internal/handler/admin/user_handler.go
View file @
e7bc6250
...
...
@@ -84,9 +84,9 @@ func (h *UserHandler) List(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
User
,
0
,
len
(
users
))
out
:=
make
([]
dto
.
Admin
User
,
0
,
len
(
users
))
for
i
:=
range
users
{
out
=
append
(
out
,
*
dto
.
UserFromService
(
&
users
[
i
]))
out
=
append
(
out
,
*
dto
.
UserFromService
Admin
(
&
users
[
i
]))
}
response
.
Paginated
(
c
,
out
,
total
,
page
,
pageSize
)
}
...
...
@@ -129,7 +129,7 @@ func (h *UserHandler) GetByID(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
user
))
response
.
Success
(
c
,
dto
.
UserFromService
Admin
(
user
))
}
// Create handles creating a new user
...
...
@@ -155,7 +155,7 @@ func (h *UserHandler) Create(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
user
))
response
.
Success
(
c
,
dto
.
UserFromService
Admin
(
user
))
}
// Update handles updating a user
...
...
@@ -189,7 +189,7 @@ func (h *UserHandler) Update(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
user
))
response
.
Success
(
c
,
dto
.
UserFromService
Admin
(
user
))
}
// Delete handles deleting a user
...
...
@@ -231,7 +231,7 @@ func (h *UserHandler) UpdateBalance(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
user
))
response
.
Success
(
c
,
dto
.
UserFromService
Admin
(
user
))
}
// GetUserAPIKeys handles getting user's API keys
...
...
backend/internal/handler/dto/mappers.go
View file @
e7bc6250
...
...
@@ -15,7 +15,6 @@ func UserFromServiceShallow(u *service.User) *User {
ID
:
u
.
ID
,
Email
:
u
.
Email
,
Username
:
u
.
Username
,
Notes
:
u
.
Notes
,
Role
:
u
.
Role
,
Balance
:
u
.
Balance
,
Concurrency
:
u
.
Concurrency
,
...
...
@@ -48,6 +47,22 @@ func UserFromService(u *service.User) *User {
return
out
}
// UserFromServiceAdmin converts a service User to DTO for admin users.
// It includes notes - user-facing endpoints must not use this.
func
UserFromServiceAdmin
(
u
*
service
.
User
)
*
AdminUser
{
if
u
==
nil
{
return
nil
}
base
:=
UserFromService
(
u
)
if
base
==
nil
{
return
nil
}
return
&
AdminUser
{
User
:
*
base
,
Notes
:
u
.
Notes
,
}
}
func
APIKeyFromService
(
k
*
service
.
APIKey
)
*
APIKey
{
if
k
==
nil
{
return
nil
...
...
@@ -72,36 +87,29 @@ func GroupFromServiceShallow(g *service.Group) *Group {
if
g
==
nil
{
return
nil
}
return
&
Group
{
ID
:
g
.
ID
,
Name
:
g
.
Name
,
Description
:
g
.
Description
,
Platform
:
g
.
Platform
,
RateMultiplier
:
g
.
RateMultiplier
,
IsExclusive
:
g
.
IsExclusive
,
Status
:
g
.
Status
,
SubscriptionType
:
g
.
SubscriptionType
,
DailyLimitUSD
:
g
.
DailyLimitUSD
,
WeeklyLimitUSD
:
g
.
WeeklyLimitUSD
,
MonthlyLimitUSD
:
g
.
MonthlyLimitUSD
,
ImagePrice1K
:
g
.
ImagePrice1K
,
ImagePrice2K
:
g
.
ImagePrice2K
,
ImagePrice4K
:
g
.
ImagePrice4K
,
ClaudeCodeOnly
:
g
.
ClaudeCodeOnly
,
FallbackGroupID
:
g
.
FallbackGroupID
,
ModelRouting
:
g
.
ModelRouting
,
ModelRoutingEnabled
:
g
.
ModelRoutingEnabled
,
CreatedAt
:
g
.
CreatedAt
,
UpdatedAt
:
g
.
UpdatedAt
,
AccountCount
:
g
.
AccountCount
,
}
out
:=
groupFromServiceBase
(
g
)
return
&
out
}
func
GroupFromService
(
g
*
service
.
Group
)
*
Group
{
if
g
==
nil
{
return
nil
}
out
:=
GroupFromServiceShallow
(
g
)
return
GroupFromServiceShallow
(
g
)
}
// GroupFromServiceAdmin converts a service Group to DTO for admin users.
// It includes internal fields like model_routing and account_count.
func
GroupFromServiceAdmin
(
g
*
service
.
Group
)
*
AdminGroup
{
if
g
==
nil
{
return
nil
}
out
:=
&
AdminGroup
{
Group
:
groupFromServiceBase
(
g
),
ModelRouting
:
g
.
ModelRouting
,
ModelRoutingEnabled
:
g
.
ModelRoutingEnabled
,
AccountCount
:
g
.
AccountCount
,
}
if
len
(
g
.
AccountGroups
)
>
0
{
out
.
AccountGroups
=
make
([]
AccountGroup
,
0
,
len
(
g
.
AccountGroups
))
for
i
:=
range
g
.
AccountGroups
{
...
...
@@ -112,6 +120,29 @@ func GroupFromService(g *service.Group) *Group {
return
out
}
func
groupFromServiceBase
(
g
*
service
.
Group
)
Group
{
return
Group
{
ID
:
g
.
ID
,
Name
:
g
.
Name
,
Description
:
g
.
Description
,
Platform
:
g
.
Platform
,
RateMultiplier
:
g
.
RateMultiplier
,
IsExclusive
:
g
.
IsExclusive
,
Status
:
g
.
Status
,
SubscriptionType
:
g
.
SubscriptionType
,
DailyLimitUSD
:
g
.
DailyLimitUSD
,
WeeklyLimitUSD
:
g
.
WeeklyLimitUSD
,
MonthlyLimitUSD
:
g
.
MonthlyLimitUSD
,
ImagePrice1K
:
g
.
ImagePrice1K
,
ImagePrice2K
:
g
.
ImagePrice2K
,
ImagePrice4K
:
g
.
ImagePrice4K
,
ClaudeCodeOnly
:
g
.
ClaudeCodeOnly
,
FallbackGroupID
:
g
.
FallbackGroupID
,
CreatedAt
:
g
.
CreatedAt
,
UpdatedAt
:
g
.
UpdatedAt
,
}
}
func
AccountFromServiceShallow
(
a
*
service
.
Account
)
*
Account
{
if
a
==
nil
{
return
nil
...
...
@@ -273,7 +304,24 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
if
rc
==
nil
{
return
nil
}
return
&
RedeemCode
{
out
:=
redeemCodeFromServiceBase
(
rc
)
return
&
out
}
// RedeemCodeFromServiceAdmin converts a service RedeemCode to DTO for admin users.
// It includes notes - user-facing endpoints must not use this.
func
RedeemCodeFromServiceAdmin
(
rc
*
service
.
RedeemCode
)
*
AdminRedeemCode
{
if
rc
==
nil
{
return
nil
}
return
&
AdminRedeemCode
{
RedeemCode
:
redeemCodeFromServiceBase
(
rc
),
Notes
:
rc
.
Notes
,
}
}
func
redeemCodeFromServiceBase
(
rc
*
service
.
RedeemCode
)
RedeemCode
{
return
RedeemCode
{
ID
:
rc
.
ID
,
Code
:
rc
.
Code
,
Type
:
rc
.
Type
,
...
...
@@ -281,7 +329,6 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
Status
:
rc
.
Status
,
UsedBy
:
rc
.
UsedBy
,
UsedAt
:
rc
.
UsedAt
,
Notes
:
rc
.
Notes
,
CreatedAt
:
rc
.
CreatedAt
,
GroupID
:
rc
.
GroupID
,
ValidityDays
:
rc
.
ValidityDays
,
...
...
@@ -302,14 +349,9 @@ func AccountSummaryFromService(a *service.Account) *AccountSummary {
}
}
// usageLogFromServiceBase is a helper that converts service UsageLog to DTO.
// The account parameter allows caller to control what Account info is included.
// The includeIPAddress parameter controls whether to include the IP address (admin-only).
func
usageLogFromServiceBase
(
l
*
service
.
UsageLog
,
account
*
AccountSummary
,
includeIPAddress
bool
)
*
UsageLog
{
if
l
==
nil
{
return
nil
}
result
:=
&
UsageLog
{
func
usageLogFromServiceUser
(
l
*
service
.
UsageLog
)
UsageLog
{
// 普通用户 DTO:严禁包含管理员字段(例如 account_rate_multiplier、ip_address、account)。
return
UsageLog
{
ID
:
l
.
ID
,
UserID
:
l
.
UserID
,
APIKeyID
:
l
.
APIKeyID
,
...
...
@@ -331,7 +373,6 @@ func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary, inclu
TotalCost
:
l
.
TotalCost
,
ActualCost
:
l
.
ActualCost
,
RateMultiplier
:
l
.
RateMultiplier
,
AccountRateMultiplier
:
l
.
AccountRateMultiplier
,
BillingType
:
l
.
BillingType
,
Stream
:
l
.
Stream
,
DurationMs
:
l
.
DurationMs
,
...
...
@@ -342,30 +383,33 @@ func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary, inclu
CreatedAt
:
l
.
CreatedAt
,
User
:
UserFromServiceShallow
(
l
.
User
),
APIKey
:
APIKeyFromService
(
l
.
APIKey
),
Account
:
account
,
Group
:
GroupFromServiceShallow
(
l
.
Group
),
Subscription
:
UserSubscriptionFromService
(
l
.
Subscription
),
}
// IP 地址仅对管理员可见
if
includeIPAddress
{
result
.
IPAddress
=
l
.
IPAddress
}
return
result
}
// UsageLogFromService converts a service UsageLog to DTO for regular users.
// It excludes Account details and IP address - users should not see these.
func
UsageLogFromService
(
l
*
service
.
UsageLog
)
*
UsageLog
{
return
usageLogFromServiceBase
(
l
,
nil
,
false
)
if
l
==
nil
{
return
nil
}
u
:=
usageLogFromServiceUser
(
l
)
return
&
u
}
// UsageLogFromServiceAdmin converts a service UsageLog to DTO for admin users.
// It includes minimal Account info (ID, Name only) and IP address.
func
UsageLogFromServiceAdmin
(
l
*
service
.
UsageLog
)
*
UsageLog
{
func
UsageLogFromServiceAdmin
(
l
*
service
.
UsageLog
)
*
Admin
UsageLog
{
if
l
==
nil
{
return
nil
}
return
usageLogFromServiceBase
(
l
,
AccountSummaryFromService
(
l
.
Account
),
true
)
return
&
AdminUsageLog
{
UsageLog
:
usageLogFromServiceUser
(
l
),
AccountRateMultiplier
:
l
.
AccountRateMultiplier
,
IPAddress
:
l
.
IPAddress
,
Account
:
AccountSummaryFromService
(
l
.
Account
),
}
}
func
UsageCleanupTaskFromService
(
task
*
service
.
UsageCleanupTask
)
*
UsageCleanupTask
{
...
...
@@ -414,7 +458,27 @@ func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscriptio
if
sub
==
nil
{
return
nil
}
return
&
UserSubscription
{
out
:=
userSubscriptionFromServiceBase
(
sub
)
return
&
out
}
// UserSubscriptionFromServiceAdmin converts a service UserSubscription to DTO for admin users.
// It includes assignment metadata and notes.
func
UserSubscriptionFromServiceAdmin
(
sub
*
service
.
UserSubscription
)
*
AdminUserSubscription
{
if
sub
==
nil
{
return
nil
}
return
&
AdminUserSubscription
{
UserSubscription
:
userSubscriptionFromServiceBase
(
sub
),
AssignedBy
:
sub
.
AssignedBy
,
AssignedAt
:
sub
.
AssignedAt
,
Notes
:
sub
.
Notes
,
AssignedByUser
:
UserFromServiceShallow
(
sub
.
AssignedByUser
),
}
}
func
userSubscriptionFromServiceBase
(
sub
*
service
.
UserSubscription
)
UserSubscription
{
return
UserSubscription
{
ID
:
sub
.
ID
,
UserID
:
sub
.
UserID
,
GroupID
:
sub
.
GroupID
,
...
...
@@ -427,14 +491,10 @@ func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscriptio
DailyUsageUSD
:
sub
.
DailyUsageUSD
,
WeeklyUsageUSD
:
sub
.
WeeklyUsageUSD
,
MonthlyUsageUSD
:
sub
.
MonthlyUsageUSD
,
AssignedBy
:
sub
.
AssignedBy
,
AssignedAt
:
sub
.
AssignedAt
,
Notes
:
sub
.
Notes
,
CreatedAt
:
sub
.
CreatedAt
,
UpdatedAt
:
sub
.
UpdatedAt
,
User
:
UserFromServiceShallow
(
sub
.
User
),
Group
:
GroupFromServiceShallow
(
sub
.
Group
),
AssignedByUser
:
UserFromServiceShallow
(
sub
.
AssignedByUser
),
}
}
...
...
@@ -442,9 +502,9 @@ func BulkAssignResultFromService(r *service.BulkAssignResult) *BulkAssignResult
if
r
==
nil
{
return
nil
}
subs
:=
make
([]
UserSubscription
,
0
,
len
(
r
.
Subscriptions
))
subs
:=
make
([]
Admin
UserSubscription
,
0
,
len
(
r
.
Subscriptions
))
for
i
:=
range
r
.
Subscriptions
{
subs
=
append
(
subs
,
*
UserSubscriptionFromService
(
&
r
.
Subscriptions
[
i
]))
subs
=
append
(
subs
,
*
UserSubscriptionFromService
Admin
(
&
r
.
Subscriptions
[
i
]))
}
return
&
BulkAssignResult
{
SuccessCount
:
r
.
SuccessCount
,
...
...
backend/internal/handler/dto/types.go
View file @
e7bc6250
...
...
@@ -6,7 +6,6 @@ type User struct {
ID
int64
`json:"id"`
Email
string
`json:"email"`
Username
string
`json:"username"`
Notes
string
`json:"notes"`
Role
string
`json:"role"`
Balance
float64
`json:"balance"`
Concurrency
int
`json:"concurrency"`
...
...
@@ -19,6 +18,14 @@ type User struct {
Subscriptions
[]
UserSubscription
`json:"subscriptions,omitempty"`
}
// AdminUser 是管理员接口使用的 user DTO(包含敏感/内部字段)。
// 注意:普通用户接口不得返回 notes 等管理员备注信息。
type
AdminUser
struct
{
User
Notes
string
`json:"notes"`
}
type
APIKey
struct
{
ID
int64
`json:"id"`
UserID
int64
`json:"user_id"`
...
...
@@ -58,13 +65,19 @@ type Group struct {
ClaudeCodeOnly
bool
`json:"claude_code_only"`
FallbackGroupID
*
int64
`json:"fallback_group_id"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
}
// AdminGroup 是管理员接口使用的 group DTO(包含敏感/内部字段)。
// 注意:普通用户接口不得返回 model_routing/account_count/account_groups 等内部信息。
type
AdminGroup
struct
{
Group
// 模型路由配置(仅 anthropic 平台使用)
ModelRouting
map
[
string
][]
int64
`json:"model_routing"`
ModelRoutingEnabled
bool
`json:"model_routing_enabled"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
AccountGroups
[]
AccountGroup
`json:"account_groups,omitempty"`
AccountCount
int64
`json:"account_count,omitempty"`
}
...
...
@@ -180,7 +193,6 @@ type RedeemCode struct {
Status
string
`json:"status"`
UsedBy
*
int64
`json:"used_by"`
UsedAt
*
time
.
Time
`json:"used_at"`
Notes
string
`json:"notes"`
CreatedAt
time
.
Time
`json:"created_at"`
GroupID
*
int64
`json:"group_id"`
...
...
@@ -190,6 +202,15 @@ type RedeemCode struct {
Group
*
Group
`json:"group,omitempty"`
}
// AdminRedeemCode 是管理员接口使用的 redeem code DTO(包含 notes 等字段)。
// 注意:普通用户接口不得返回 notes 等内部信息。
type
AdminRedeemCode
struct
{
RedeemCode
Notes
string
`json:"notes"`
}
// UsageLog 是普通用户接口使用的 usage log DTO(不包含管理员字段)。
type
UsageLog
struct
{
ID
int64
`json:"id"`
UserID
int64
`json:"user_id"`
...
...
@@ -209,14 +230,13 @@ type UsageLog struct {
CacheCreation5mTokens
int
`json:"cache_creation_5m_tokens"`
CacheCreation1hTokens
int
`json:"cache_creation_1h_tokens"`
InputCost
float64
`json:"input_cost"`
OutputCost
float64
`json:"output_cost"`
CacheCreationCost
float64
`json:"cache_creation_cost"`
CacheReadCost
float64
`json:"cache_read_cost"`
TotalCost
float64
`json:"total_cost"`
ActualCost
float64
`json:"actual_cost"`
RateMultiplier
float64
`json:"rate_multiplier"`
AccountRateMultiplier
*
float64
`json:"account_rate_multiplier"`
InputCost
float64
`json:"input_cost"`
OutputCost
float64
`json:"output_cost"`
CacheCreationCost
float64
`json:"cache_creation_cost"`
CacheReadCost
float64
`json:"cache_read_cost"`
TotalCost
float64
`json:"total_cost"`
ActualCost
float64
`json:"actual_cost"`
RateMultiplier
float64
`json:"rate_multiplier"`
BillingType
int8
`json:"billing_type"`
Stream
bool
`json:"stream"`
...
...
@@ -230,18 +250,28 @@ type UsageLog struct {
// User-Agent
UserAgent
*
string
`json:"user_agent"`
// IP 地址(仅管理员可见)
IPAddress
*
string
`json:"ip_address,omitempty"`
CreatedAt
time
.
Time
`json:"created_at"`
User
*
User
`json:"user,omitempty"`
APIKey
*
APIKey
`json:"api_key,omitempty"`
Account
*
AccountSummary
`json:"account,omitempty"`
// Use minimal AccountSummary to prevent data leakage
Group
*
Group
`json:"group,omitempty"`
Subscription
*
UserSubscription
`json:"subscription,omitempty"`
}
// AdminUsageLog 是管理员接口使用的 usage log DTO(包含管理员字段)。
type
AdminUsageLog
struct
{
UsageLog
// AccountRateMultiplier 账号计费倍率快照(nil 表示按 1.0 处理)
AccountRateMultiplier
*
float64
`json:"account_rate_multiplier"`
// IPAddress 用户请求 IP(仅管理员可见)
IPAddress
*
string
`json:"ip_address,omitempty"`
// Account 最小账号信息(避免泄露敏感字段)
Account
*
AccountSummary
`json:"account,omitempty"`
}
type
UsageCleanupFilters
struct
{
StartTime
time
.
Time
`json:"start_time"`
EndTime
time
.
Time
`json:"end_time"`
...
...
@@ -300,23 +330,30 @@ type UserSubscription struct {
WeeklyUsageUSD
float64
`json:"weekly_usage_usd"`
MonthlyUsageUSD
float64
`json:"monthly_usage_usd"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
User
*
User
`json:"user,omitempty"`
Group
*
Group
`json:"group,omitempty"`
}
// AdminUserSubscription 是管理员接口使用的订阅 DTO(包含分配信息/备注等字段)。
// 注意:普通用户接口不得返回 assigned_by/assigned_at/notes/assigned_by_user 等管理员字段。
type
AdminUserSubscription
struct
{
UserSubscription
AssignedBy
*
int64
`json:"assigned_by"`
AssignedAt
time
.
Time
`json:"assigned_at"`
Notes
string
`json:"notes"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
User
*
User
`json:"user,omitempty"`
Group
*
Group
`json:"group,omitempty"`
AssignedByUser
*
User
`json:"assigned_by_user,omitempty"`
AssignedByUser
*
User
`json:"assigned_by_user,omitempty"`
}
type
BulkAssignResult
struct
{
SuccessCount
int
`json:"success_count"`
FailedCount
int
`json:"failed_count"`
Subscriptions
[]
UserSubscription
`json:"subscriptions"`
Errors
[]
string
`json:"errors"`
SuccessCount
int
`json:"success_count"`
FailedCount
int
`json:"failed_count"`
Subscriptions
[]
Admin
UserSubscription
`json:"subscriptions"`
Errors
[]
string
`json:"errors"`
}
// PromoCode 注册优惠码
...
...
backend/internal/handler/user_handler.go
View file @
e7bc6250
...
...
@@ -47,9 +47,6 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
return
}
// 清空notes字段,普通用户不应看到备注
userData
.
Notes
=
""
response
.
Success
(
c
,
dto
.
UserFromService
(
userData
))
}
...
...
@@ -105,8 +102,5 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
return
}
// 清空notes字段,普通用户不应看到备注
updatedUser
.
Notes
=
""
response
.
Success
(
c
,
dto
.
UserFromService
(
updatedUser
))
}
backend/internal/server/api_contract_test.go
View file @
e7bc6250
...
...
@@ -51,7 +51,6 @@ func TestAPIContracts(t *testing.T) {
"id": 1,
"email": "alice@example.com",
"username": "alice",
"notes": "hello",
"role": "user",
"balance": 12.5,
"concurrency": 5,
...
...
@@ -131,6 +130,153 @@ func TestAPIContracts(t *testing.T) {
}
}`
,
},
{
name
:
"GET /api/v1/groups/available"
,
setup
:
func
(
t
*
testing
.
T
,
deps
*
contractDeps
)
{
t
.
Helper
()
// 普通用户可见的分组列表不应包含内部字段(如 model_routing/account_count)。
deps
.
groupRepo
.
SetActive
([]
service
.
Group
{
{
ID
:
10
,
Name
:
"Group One"
,
Description
:
"desc"
,
Platform
:
service
.
PlatformAnthropic
,
RateMultiplier
:
1.5
,
IsExclusive
:
false
,
Status
:
service
.
StatusActive
,
SubscriptionType
:
service
.
SubscriptionTypeStandard
,
ModelRoutingEnabled
:
true
,
ModelRouting
:
map
[
string
][]
int64
{
"claude-3-*"
:
[]
int64
{
101
,
102
},
},
AccountCount
:
2
,
CreatedAt
:
deps
.
now
,
UpdatedAt
:
deps
.
now
,
},
})
deps
.
userSubRepo
.
SetActiveByUserID
(
1
,
nil
)
},
method
:
http
.
MethodGet
,
path
:
"/api/v1/groups/available"
,
wantStatus
:
http
.
StatusOK
,
wantJSON
:
`{
"code": 0,
"message": "success",
"data": [
{
"id": 10,
"name": "Group One",
"description": "desc",
"platform": "anthropic",
"rate_multiplier": 1.5,
"is_exclusive": false,
"status": "active",
"subscription_type": "standard",
"daily_limit_usd": null,
"weekly_limit_usd": null,
"monthly_limit_usd": null,
"image_price_1k": null,
"image_price_2k": null,
"image_price_4k": null,
"claude_code_only": false,
"fallback_group_id": null,
"created_at": "2025-01-02T03:04:05Z",
"updated_at": "2025-01-02T03:04:05Z"
}
]
}`
,
},
{
name
:
"GET /api/v1/subscriptions"
,
setup
:
func
(
t
*
testing
.
T
,
deps
*
contractDeps
)
{
t
.
Helper
()
// 普通用户订阅接口不应包含 assigned_* / notes 等管理员字段。
deps
.
userSubRepo
.
SetByUserID
(
1
,
[]
service
.
UserSubscription
{
{
ID
:
501
,
UserID
:
1
,
GroupID
:
10
,
StartsAt
:
deps
.
now
,
ExpiresAt
:
deps
.
now
.
Add
(
24
*
time
.
Hour
),
Status
:
service
.
SubscriptionStatusActive
,
DailyUsageUSD
:
1.23
,
WeeklyUsageUSD
:
2.34
,
MonthlyUsageUSD
:
3.45
,
AssignedBy
:
ptr
(
int64
(
999
)),
AssignedAt
:
deps
.
now
,
Notes
:
"admin-note"
,
CreatedAt
:
deps
.
now
,
UpdatedAt
:
deps
.
now
,
},
})
},
method
:
http
.
MethodGet
,
path
:
"/api/v1/subscriptions"
,
wantStatus
:
http
.
StatusOK
,
wantJSON
:
`{
"code": 0,
"message": "success",
"data": [
{
"id": 501,
"user_id": 1,
"group_id": 10,
"starts_at": "2025-01-02T03:04:05Z",
"expires_at": "2025-01-03T03:04:05Z",
"status": "active",
"daily_window_start": null,
"weekly_window_start": null,
"monthly_window_start": null,
"daily_usage_usd": 1.23,
"weekly_usage_usd": 2.34,
"monthly_usage_usd": 3.45,
"created_at": "2025-01-02T03:04:05Z",
"updated_at": "2025-01-02T03:04:05Z"
}
]
}`
,
},
{
name
:
"GET /api/v1/redeem/history"
,
setup
:
func
(
t
*
testing
.
T
,
deps
*
contractDeps
)
{
t
.
Helper
()
// 普通用户兑换历史不应包含 notes 等内部字段。
deps
.
redeemRepo
.
SetByUser
(
1
,
[]
service
.
RedeemCode
{
{
ID
:
900
,
Code
:
"CODE-123"
,
Type
:
service
.
RedeemTypeBalance
,
Value
:
1.25
,
Status
:
service
.
StatusUsed
,
UsedBy
:
ptr
(
int64
(
1
)),
UsedAt
:
ptr
(
deps
.
now
),
Notes
:
"internal-note"
,
CreatedAt
:
deps
.
now
,
},
})
},
method
:
http
.
MethodGet
,
path
:
"/api/v1/redeem/history"
,
wantStatus
:
http
.
StatusOK
,
wantJSON
:
`{
"code": 0,
"message": "success",
"data": [
{
"id": 900,
"code": "CODE-123",
"type": "balance",
"value": 1.25,
"status": "used",
"used_by": 1,
"used_at": "2025-01-02T03:04:05Z",
"created_at": "2025-01-02T03:04:05Z",
"group_id": null,
"validity_days": 0
}
]
}`
,
},
{
name
:
"GET /api/v1/usage/stats"
,
setup
:
func
(
t
*
testing
.
T
,
deps
*
contractDeps
)
{
...
...
@@ -190,24 +336,25 @@ func TestAPIContracts(t *testing.T) {
t
.
Helper
()
deps
.
usageRepo
.
SetUserLogs
(
1
,
[]
service
.
UsageLog
{
{
ID
:
1
,
UserID
:
1
,
APIKeyID
:
100
,
AccountID
:
200
,
RequestID
:
"req_123"
,
Model
:
"claude-3"
,
InputTokens
:
10
,
OutputTokens
:
20
,
CacheCreationTokens
:
1
,
CacheReadTokens
:
2
,
TotalCost
:
0.5
,
ActualCost
:
0.5
,
RateMultiplier
:
1
,
BillingType
:
service
.
BillingTypeBalance
,
Stream
:
true
,
DurationMs
:
ptr
(
100
),
FirstTokenMs
:
ptr
(
50
),
CreatedAt
:
deps
.
now
,
ID
:
1
,
UserID
:
1
,
APIKeyID
:
100
,
AccountID
:
200
,
AccountRateMultiplier
:
ptr
(
0.5
),
RequestID
:
"req_123"
,
Model
:
"claude-3"
,
InputTokens
:
10
,
OutputTokens
:
20
,
CacheCreationTokens
:
1
,
CacheReadTokens
:
2
,
TotalCost
:
0.5
,
ActualCost
:
0.5
,
RateMultiplier
:
1
,
BillingType
:
service
.
BillingTypeBalance
,
Stream
:
true
,
DurationMs
:
ptr
(
100
),
FirstTokenMs
:
ptr
(
50
),
CreatedAt
:
deps
.
now
,
},
})
},
...
...
@@ -238,10 +385,9 @@ func TestAPIContracts(t *testing.T) {
"output_cost": 0,
"cache_creation_cost": 0,
"cache_read_cost": 0,
"total_cost": 0.5,
"total_cost": 0.5,
"actual_cost": 0.5,
"rate_multiplier": 1,
"account_rate_multiplier": null,
"billing_type": 0,
"stream": true,
"duration_ms": 100,
...
...
@@ -386,8 +532,11 @@ type contractDeps struct {
now
time
.
Time
router
http
.
Handler
apiKeyRepo
*
stubApiKeyRepo
groupRepo
*
stubGroupRepo
userSubRepo
*
stubUserSubscriptionRepo
usageRepo
*
stubUsageLogRepo
settingRepo
*
stubSettingRepo
redeemRepo
*
stubRedeemCodeRepo
}
func
newContractDeps
(
t
*
testing
.
T
)
*
contractDeps
{
...
...
@@ -415,11 +564,11 @@ func newContractDeps(t *testing.T) *contractDeps {
apiKeyRepo
:=
newStubApiKeyRepo
(
now
)
apiKeyCache
:=
stubApiKeyCache
{}
groupRepo
:=
stubGroupRepo
{}
userSubRepo
:=
stubUserSubscriptionRepo
{}
groupRepo
:=
&
stubGroupRepo
{}
userSubRepo
:=
&
stubUserSubscriptionRepo
{}
accountRepo
:=
stubAccountRepo
{}
proxyRepo
:=
stubProxyRepo
{}
redeemRepo
:=
stubRedeemCodeRepo
{}
redeemRepo
:=
&
stubRedeemCodeRepo
{}
cfg
:=
&
config
.
Config
{
Default
:
config
.
DefaultConfig
{
...
...
@@ -434,6 +583,12 @@ func newContractDeps(t *testing.T) *contractDeps {
usageRepo
:=
newStubUsageLogRepo
()
usageService
:=
service
.
NewUsageService
(
usageRepo
,
userRepo
,
nil
,
nil
)
subscriptionService
:=
service
.
NewSubscriptionService
(
groupRepo
,
userSubRepo
,
nil
)
subscriptionHandler
:=
handler
.
NewSubscriptionHandler
(
subscriptionService
)
redeemService
:=
service
.
NewRedeemService
(
redeemRepo
,
userRepo
,
subscriptionService
,
nil
,
nil
,
nil
,
nil
)
redeemHandler
:=
handler
.
NewRedeemHandler
(
redeemService
)
settingRepo
:=
newStubSettingRepo
()
settingService
:=
service
.
NewSettingService
(
settingRepo
,
cfg
)
...
...
@@ -473,12 +628,21 @@ func newContractDeps(t *testing.T) *contractDeps {
v1Keys
.
Use
(
jwtAuth
)
v1Keys
.
GET
(
"/keys"
,
apiKeyHandler
.
List
)
v1Keys
.
POST
(
"/keys"
,
apiKeyHandler
.
Create
)
v1Keys
.
GET
(
"/groups/available"
,
apiKeyHandler
.
GetAvailableGroups
)
v1Usage
:=
v1
.
Group
(
""
)
v1Usage
.
Use
(
jwtAuth
)
v1Usage
.
GET
(
"/usage"
,
usageHandler
.
List
)
v1Usage
.
GET
(
"/usage/stats"
,
usageHandler
.
Stats
)
v1Subs
:=
v1
.
Group
(
""
)
v1Subs
.
Use
(
jwtAuth
)
v1Subs
.
GET
(
"/subscriptions"
,
subscriptionHandler
.
List
)
v1Redeem
:=
v1
.
Group
(
""
)
v1Redeem
.
Use
(
jwtAuth
)
v1Redeem
.
GET
(
"/redeem/history"
,
redeemHandler
.
GetHistory
)
v1Admin
:=
v1
.
Group
(
"/admin"
)
v1Admin
.
Use
(
adminAuth
)
v1Admin
.
GET
(
"/settings"
,
adminSettingHandler
.
GetSettings
)
...
...
@@ -488,8 +652,11 @@ func newContractDeps(t *testing.T) *contractDeps {
now
:
now
,
router
:
r
,
apiKeyRepo
:
apiKeyRepo
,
groupRepo
:
groupRepo
,
userSubRepo
:
userSubRepo
,
usageRepo
:
usageRepo
,
settingRepo
:
settingRepo
,
redeemRepo
:
redeemRepo
,
}
}
...
...
@@ -627,7 +794,13 @@ func (stubApiKeyCache) SubscribeAuthCacheInvalidation(ctx context.Context, handl
return
nil
}
type
stubGroupRepo
struct
{}
type
stubGroupRepo
struct
{
active
[]
service
.
Group
}
func
(
r
*
stubGroupRepo
)
SetActive
(
groups
[]
service
.
Group
)
{
r
.
active
=
append
([]
service
.
Group
(
nil
),
groups
...
)
}
func
(
stubGroupRepo
)
Create
(
ctx
context
.
Context
,
group
*
service
.
Group
)
error
{
return
errors
.
New
(
"not implemented"
)
...
...
@@ -661,12 +834,19 @@ func (stubGroupRepo) ListWithFilters(ctx context.Context, params pagination.Pagi
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
}
func
(
stubGroupRepo
)
ListActive
(
ctx
context
.
Context
)
([]
service
.
Group
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
func
(
r
*
stubGroupRepo
)
ListActive
(
ctx
context
.
Context
)
([]
service
.
Group
,
error
)
{
return
append
([]
service
.
Group
(
nil
),
r
.
active
...
),
nil
}
func
(
stubGroupRepo
)
ListActiveByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
service
.
Group
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
func
(
r
*
stubGroupRepo
)
ListActiveByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
service
.
Group
,
error
)
{
out
:=
make
([]
service
.
Group
,
0
,
len
(
r
.
active
))
for
i
:=
range
r
.
active
{
g
:=
r
.
active
[
i
]
if
g
.
Platform
==
platform
{
out
=
append
(
out
,
g
)
}
}
return
out
,
nil
}
func
(
stubGroupRepo
)
ExistsByName
(
ctx
context
.
Context
,
name
string
)
(
bool
,
error
)
{
...
...
@@ -884,7 +1064,16 @@ func (stubProxyRepo) ListAccountSummariesByProxyID(ctx context.Context, proxyID
return
nil
,
errors
.
New
(
"not implemented"
)
}
type
stubRedeemCodeRepo
struct
{}
type
stubRedeemCodeRepo
struct
{
byUser
map
[
int64
][]
service
.
RedeemCode
}
func
(
r
*
stubRedeemCodeRepo
)
SetByUser
(
userID
int64
,
codes
[]
service
.
RedeemCode
)
{
if
r
.
byUser
==
nil
{
r
.
byUser
=
make
(
map
[
int64
][]
service
.
RedeemCode
)
}
r
.
byUser
[
userID
]
=
append
([]
service
.
RedeemCode
(
nil
),
codes
...
)
}
func
(
stubRedeemCodeRepo
)
Create
(
ctx
context
.
Context
,
code
*
service
.
RedeemCode
)
error
{
return
errors
.
New
(
"not implemented"
)
...
...
@@ -922,11 +1111,35 @@ func (stubRedeemCodeRepo) ListWithFilters(ctx context.Context, params pagination
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
}
func
(
stubRedeemCodeRepo
)
ListByUser
(
ctx
context
.
Context
,
userID
int64
,
limit
int
)
([]
service
.
RedeemCode
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
func
(
r
*
stubRedeemCodeRepo
)
ListByUser
(
ctx
context
.
Context
,
userID
int64
,
limit
int
)
([]
service
.
RedeemCode
,
error
)
{
if
r
.
byUser
==
nil
{
return
nil
,
nil
}
codes
:=
r
.
byUser
[
userID
]
if
limit
>
0
&&
len
(
codes
)
>
limit
{
codes
=
codes
[
:
limit
]
}
return
append
([]
service
.
RedeemCode
(
nil
),
codes
...
),
nil
}
type
stubUserSubscriptionRepo
struct
{}
type
stubUserSubscriptionRepo
struct
{
byUser
map
[
int64
][]
service
.
UserSubscription
activeByUser
map
[
int64
][]
service
.
UserSubscription
}
func
(
r
*
stubUserSubscriptionRepo
)
SetByUserID
(
userID
int64
,
subs
[]
service
.
UserSubscription
)
{
if
r
.
byUser
==
nil
{
r
.
byUser
=
make
(
map
[
int64
][]
service
.
UserSubscription
)
}
r
.
byUser
[
userID
]
=
append
([]
service
.
UserSubscription
(
nil
),
subs
...
)
}
func
(
r
*
stubUserSubscriptionRepo
)
SetActiveByUserID
(
userID
int64
,
subs
[]
service
.
UserSubscription
)
{
if
r
.
activeByUser
==
nil
{
r
.
activeByUser
=
make
(
map
[
int64
][]
service
.
UserSubscription
)
}
r
.
activeByUser
[
userID
]
=
append
([]
service
.
UserSubscription
(
nil
),
subs
...
)
}
func
(
stubUserSubscriptionRepo
)
Create
(
ctx
context
.
Context
,
sub
*
service
.
UserSubscription
)
error
{
return
errors
.
New
(
"not implemented"
)
...
...
@@ -946,11 +1159,17 @@ func (stubUserSubscriptionRepo) Update(ctx context.Context, sub *service.UserSub
func
(
stubUserSubscriptionRepo
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
return
errors
.
New
(
"not implemented"
)
}
func
(
stubUserSubscriptionRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
)
([]
service
.
UserSubscription
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
func
(
r
*
stubUserSubscriptionRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
)
([]
service
.
UserSubscription
,
error
)
{
if
r
.
byUser
==
nil
{
return
nil
,
nil
}
return
append
([]
service
.
UserSubscription
(
nil
),
r
.
byUser
[
userID
]
...
),
nil
}
func
(
stubUserSubscriptionRepo
)
ListActiveByUserID
(
ctx
context
.
Context
,
userID
int64
)
([]
service
.
UserSubscription
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
func
(
r
*
stubUserSubscriptionRepo
)
ListActiveByUserID
(
ctx
context
.
Context
,
userID
int64
)
([]
service
.
UserSubscription
,
error
)
{
if
r
.
activeByUser
==
nil
{
return
nil
,
nil
}
return
append
([]
service
.
UserSubscription
(
nil
),
r
.
activeByUser
[
userID
]
...
),
nil
}
func
(
stubUserSubscriptionRepo
)
ListByGroupID
(
ctx
context
.
Context
,
groupID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
UserSubscription
,
*
pagination
.
PaginationResult
,
error
)
{
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
...
...
frontend/src/api/admin/groups.ts
View file @
e7bc6250
...
...
@@ -5,7 +5,7 @@
import
{
apiClient
}
from
'
../client
'
import
type
{
Group
,
Admin
Group
,
GroupPlatform
,
CreateGroupRequest
,
UpdateGroupRequest
,
...
...
@@ -31,8 +31,8 @@ export async function list(
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
Group
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
Group
>>
(
'
/admin/groups
'
,
{
):
Promise
<
PaginatedResponse
<
Admin
Group
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
Admin
Group
>>
(
'
/admin/groups
'
,
{
params
:
{
page
,
page_size
:
pageSize
,
...
...
@@ -48,8 +48,8 @@ export async function list(
* @param platform - Optional platform filter
* @returns List of all active groups
*/
export
async
function
getAll
(
platform
?:
GroupPlatform
):
Promise
<
Group
[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
Group
[]
>
(
'
/admin/groups/all
'
,
{
export
async
function
getAll
(
platform
?:
GroupPlatform
):
Promise
<
Admin
Group
[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
Admin
Group
[]
>
(
'
/admin/groups/all
'
,
{
params
:
platform
?
{
platform
}
:
undefined
})
return
data
...
...
@@ -60,7 +60,7 @@ export async function getAll(platform?: GroupPlatform): Promise<Group[]> {
* @param platform - Platform to filter by
* @returns List of groups for the specified platform
*/
export
async
function
getByPlatform
(
platform
:
GroupPlatform
):
Promise
<
Group
[]
>
{
export
async
function
getByPlatform
(
platform
:
GroupPlatform
):
Promise
<
Admin
Group
[]
>
{
return
getAll
(
platform
)
}
...
...
@@ -69,8 +69,8 @@ export async function getByPlatform(platform: GroupPlatform): Promise<Group[]> {
* @param id - Group ID
* @returns Group details
*/
export
async
function
getById
(
id
:
number
):
Promise
<
Group
>
{
const
{
data
}
=
await
apiClient
.
get
<
Group
>
(
`/admin/groups/
${
id
}
`
)
export
async
function
getById
(
id
:
number
):
Promise
<
Admin
Group
>
{
const
{
data
}
=
await
apiClient
.
get
<
Admin
Group
>
(
`/admin/groups/
${
id
}
`
)
return
data
}
...
...
@@ -79,8 +79,8 @@ export async function getById(id: number): Promise<Group> {
* @param groupData - Group data
* @returns Created group
*/
export
async
function
create
(
groupData
:
CreateGroupRequest
):
Promise
<
Group
>
{
const
{
data
}
=
await
apiClient
.
post
<
Group
>
(
'
/admin/groups
'
,
groupData
)
export
async
function
create
(
groupData
:
CreateGroupRequest
):
Promise
<
Admin
Group
>
{
const
{
data
}
=
await
apiClient
.
post
<
Admin
Group
>
(
'
/admin/groups
'
,
groupData
)
return
data
}
...
...
@@ -90,8 +90,8 @@ export async function create(groupData: CreateGroupRequest): Promise<Group> {
* @param updates - Fields to update
* @returns Updated group
*/
export
async
function
update
(
id
:
number
,
updates
:
UpdateGroupRequest
):
Promise
<
Group
>
{
const
{
data
}
=
await
apiClient
.
put
<
Group
>
(
`/admin/groups/
${
id
}
`
,
updates
)
export
async
function
update
(
id
:
number
,
updates
:
UpdateGroupRequest
):
Promise
<
Admin
Group
>
{
const
{
data
}
=
await
apiClient
.
put
<
Admin
Group
>
(
`/admin/groups/
${
id
}
`
,
updates
)
return
data
}
...
...
@@ -111,7 +111,7 @@ export async function deleteGroup(id: number): Promise<{ message: string }> {
* @param status - New status
* @returns Updated group
*/
export
async
function
toggleStatus
(
id
:
number
,
status
:
'
active
'
|
'
inactive
'
):
Promise
<
Group
>
{
export
async
function
toggleStatus
(
id
:
number
,
status
:
'
active
'
|
'
inactive
'
):
Promise
<
Admin
Group
>
{
return
update
(
id
,
{
status
})
}
...
...
frontend/src/api/admin/usage.ts
View file @
e7bc6250
...
...
@@ -4,7 +4,7 @@
*/
import
{
apiClient
}
from
'
../client
'
import
type
{
UsageLog
,
UsageQueryParams
,
PaginatedResponse
}
from
'
@/types
'
import
type
{
Admin
UsageLog
,
UsageQueryParams
,
PaginatedResponse
}
from
'
@/types
'
// ==================== Types ====================
...
...
@@ -85,8 +85,8 @@ export interface AdminUsageQueryParams extends UsageQueryParams {
export
async
function
list
(
params
:
AdminUsageQueryParams
,
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
UsageLog
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
UsageLog
>>
(
'
/admin/usage
'
,
{
):
Promise
<
PaginatedResponse
<
Admin
UsageLog
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
Admin
UsageLog
>>
(
'
/admin/usage
'
,
{
params
,
signal
:
options
?.
signal
})
...
...
frontend/src/api/admin/users.ts
View file @
e7bc6250
...
...
@@ -4,7 +4,7 @@
*/
import
{
apiClient
}
from
'
../client
'
import
type
{
User
,
UpdateUserRequest
,
PaginatedResponse
}
from
'
@/types
'
import
type
{
Admin
User
,
UpdateUserRequest
,
PaginatedResponse
}
from
'
@/types
'
/**
* List all users with pagination
...
...
@@ -26,7 +26,7 @@ export async function list(
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
User
>>
{
):
Promise
<
PaginatedResponse
<
Admin
User
>>
{
// Build params with attribute filters in attr[id]=value format
const
params
:
Record
<
string
,
any
>
=
{
page
,
...
...
@@ -44,8 +44,7 @@ export async function list(
}
}
}
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
User
>>
(
'
/admin/users
'
,
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
AdminUser
>>
(
'
/admin/users
'
,
{
params
,
signal
:
options
?.
signal
})
...
...
@@ -57,8 +56,8 @@ export async function list(
* @param id - User ID
* @returns User details
*/
export
async
function
getById
(
id
:
number
):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
get
<
User
>
(
`/admin/users/
${
id
}
`
)
export
async
function
getById
(
id
:
number
):
Promise
<
Admin
User
>
{
const
{
data
}
=
await
apiClient
.
get
<
Admin
User
>
(
`/admin/users/
${
id
}
`
)
return
data
}
...
...
@@ -73,8 +72,8 @@ export async function create(userData: {
balance
?:
number
concurrency
?:
number
allowed_groups
?:
number
[]
|
null
}):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
post
<
User
>
(
'
/admin/users
'
,
userData
)
}):
Promise
<
Admin
User
>
{
const
{
data
}
=
await
apiClient
.
post
<
Admin
User
>
(
'
/admin/users
'
,
userData
)
return
data
}
...
...
@@ -84,8 +83,8 @@ export async function create(userData: {
* @param updates - Fields to update
* @returns Updated user
*/
export
async
function
update
(
id
:
number
,
updates
:
UpdateUserRequest
):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
put
<
User
>
(
`/admin/users/
${
id
}
`
,
updates
)
export
async
function
update
(
id
:
number
,
updates
:
UpdateUserRequest
):
Promise
<
Admin
User
>
{
const
{
data
}
=
await
apiClient
.
put
<
Admin
User
>
(
`/admin/users/
${
id
}
`
,
updates
)
return
data
}
...
...
@@ -112,8 +111,8 @@ export async function updateBalance(
balance
:
number
,
operation
:
'
set
'
|
'
add
'
|
'
subtract
'
=
'
set
'
,
notes
?:
string
):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
post
<
User
>
(
`/admin/users/
${
id
}
/balance`
,
{
):
Promise
<
Admin
User
>
{
const
{
data
}
=
await
apiClient
.
post
<
Admin
User
>
(
`/admin/users/
${
id
}
/balance`
,
{
balance
,
operation
,
notes
:
notes
||
''
...
...
@@ -127,7 +126,7 @@ export async function updateBalance(
* @param concurrency - New concurrency limit
* @returns Updated user
*/
export
async
function
updateConcurrency
(
id
:
number
,
concurrency
:
number
):
Promise
<
User
>
{
export
async
function
updateConcurrency
(
id
:
number
,
concurrency
:
number
):
Promise
<
Admin
User
>
{
return
update
(
id
,
{
concurrency
})
}
...
...
@@ -137,7 +136,7 @@ export async function updateConcurrency(id: number, concurrency: number): Promis
* @param status - New status
* @returns Updated user
*/
export
async
function
toggleStatus
(
id
:
number
,
status
:
'
active
'
|
'
disabled
'
):
Promise
<
User
>
{
export
async
function
toggleStatus
(
id
:
number
,
status
:
'
active
'
|
'
disabled
'
):
Promise
<
Admin
User
>
{
return
update
(
id
,
{
status
})
}
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
e7bc6250
...
...
@@ -648,7 +648,7 @@ import { ref, watch, computed } from 'vue'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
Proxy
,
Group
}
from
'
@/types
'
import
type
{
Proxy
,
Admin
Group
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
...
...
@@ -659,7 +659,7 @@ interface Props {
show
:
boolean
accountIds
:
number
[]
proxies
:
Proxy
[]
groups
:
Group
[]
groups
:
Admin
Group
[]
}
const
props
=
defineProps
<
Props
>
()
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
e7bc6250
...
...
@@ -1816,7 +1816,7 @@ import {
import
{
useOpenAIOAuth
}
from
'
@/composables/useOpenAIOAuth
'
import
{
useGeminiOAuth
}
from
'
@/composables/useGeminiOAuth
'
import
{
useAntigravityOAuth
}
from
'
@/composables/useAntigravityOAuth
'
import
type
{
Proxy
,
Group
,
AccountPlatform
,
AccountType
}
from
'
@/types
'
import
type
{
Proxy
,
Admin
Group
,
AccountPlatform
,
AccountType
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
...
...
@@ -1862,7 +1862,7 @@ const apiKeyHint = computed(() => {
interface
Props
{
show
:
boolean
proxies
:
Proxy
[]
groups
:
Group
[]
groups
:
Admin
Group
[]
}
const
props
=
defineProps
<
Props
>
()
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
e7bc6250
...
...
@@ -883,7 +883,7 @@ import { useI18n } from 'vue-i18n'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
Account
,
Proxy
,
Group
}
from
'
@/types
'
import
type
{
Account
,
Proxy
,
Admin
Group
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
...
...
@@ -901,7 +901,7 @@ interface Props {
show
:
boolean
account
:
Account
|
null
proxies
:
Proxy
[]
groups
:
Group
[]
groups
:
Admin
Group
[]
}
const
props
=
defineProps
<
Props
>
()
...
...
frontend/src/components/admin/usage/UsageTable.vue
View file @
e7bc6250
...
...
@@ -239,7 +239,7 @@ import { formatDateTime } from '@/utils/format'
import
DataTable
from
'
@/components/common/DataTable.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
type
{
UsageLog
}
from
'
@/types
'
import
type
{
Admin
UsageLog
}
from
'
@/types
'
defineProps
([
'
data
'
,
'
loading
'
])
const
{
t
}
=
useI18n
()
...
...
@@ -247,12 +247,12 @@ const { t } = useI18n()
// Tooltip state - cost
const
tooltipVisible
=
ref
(
false
)
const
tooltipPosition
=
ref
({
x
:
0
,
y
:
0
})
const
tooltipData
=
ref
<
UsageLog
|
null
>
(
null
)
const
tooltipData
=
ref
<
Admin
UsageLog
|
null
>
(
null
)
// Tooltip state - token
const
tokenTooltipVisible
=
ref
(
false
)
const
tokenTooltipPosition
=
ref
({
x
:
0
,
y
:
0
})
const
tokenTooltipData
=
ref
<
UsageLog
|
null
>
(
null
)
const
tokenTooltipData
=
ref
<
Admin
UsageLog
|
null
>
(
null
)
const
cols
=
computed
(()
=>
[
{
key
:
'
user
'
,
label
:
t
(
'
admin.usage.user
'
),
sortable
:
false
},
...
...
@@ -296,7 +296,7 @@ const formatDuration = (ms: number | null | undefined): string => {
}
// Cost tooltip functions
const
showTooltip
=
(
event
:
MouseEvent
,
row
:
UsageLog
)
=>
{
const
showTooltip
=
(
event
:
MouseEvent
,
row
:
Admin
UsageLog
)
=>
{
const
target
=
event
.
currentTarget
as
HTMLElement
const
rect
=
target
.
getBoundingClientRect
()
tooltipData
.
value
=
row
...
...
@@ -311,7 +311,7 @@ const hideTooltip = () => {
}
// Token tooltip functions
const
showTokenTooltip
=
(
event
:
MouseEvent
,
row
:
UsageLog
)
=>
{
const
showTokenTooltip
=
(
event
:
MouseEvent
,
row
:
Admin
UsageLog
)
=>
{
const
target
=
event
.
currentTarget
as
HTMLElement
const
rect
=
target
.
getBoundingClientRect
()
tokenTooltipData
.
value
=
row
...
...
frontend/src/components/admin/user/UserAllowedGroupsModal.vue
View file @
e7bc6250
...
...
@@ -39,10 +39,10 @@ import { ref, watch } from 'vue'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
User
,
Group
}
from
'
@/types
'
import
type
{
Admin
User
,
Group
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
const
props
=
defineProps
<
{
show
:
boolean
,
user
:
User
|
null
}
>
()
const
props
=
defineProps
<
{
show
:
boolean
,
user
:
Admin
User
|
null
}
>
()
const
emit
=
defineEmits
([
'
close
'
,
'
success
'
]);
const
{
t
}
=
useI18n
();
const
appStore
=
useAppStore
()
const
groups
=
ref
<
Group
[]
>
([]);
const
selectedIds
=
ref
<
number
[]
>
([]);
const
loading
=
ref
(
false
);
const
submitting
=
ref
(
false
)
...
...
@@ -56,4 +56,4 @@ const handleSave = async () => {
appStore
.
showSuccess
(
t
(
'
admin.users.allowedGroupsUpdated
'
));
emit
(
'
success
'
);
emit
(
'
close
'
)
}
catch
(
error
)
{
console
.
error
(
'
Failed to update allowed groups:
'
,
error
)
}
finally
{
submitting
.
value
=
false
}
}
</
script
>
\ No newline at end of file
</
script
>
frontend/src/components/admin/user/UserApiKeysModal.vue
View file @
e7bc6250
...
...
@@ -32,10 +32,10 @@ import { ref, watch } from 'vue'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
formatDateTime
}
from
'
@/utils/format
'
import
type
{
User
,
ApiKey
}
from
'
@/types
'
import
type
{
Admin
User
,
ApiKey
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
const
props
=
defineProps
<
{
show
:
boolean
,
user
:
User
|
null
}
>
()
const
props
=
defineProps
<
{
show
:
boolean
,
user
:
Admin
User
|
null
}
>
()
defineEmits
([
'
close
'
]);
const
{
t
}
=
useI18n
()
const
apiKeys
=
ref
<
ApiKey
[]
>
([]);
const
loading
=
ref
(
false
)
...
...
@@ -44,4 +44,4 @@ const load = async () => {
if
(
!
props
.
user
)
return
;
loading
.
value
=
true
try
{
const
res
=
await
adminAPI
.
users
.
getUserApiKeys
(
props
.
user
.
id
);
apiKeys
.
value
=
res
.
items
||
[]
}
catch
(
error
)
{
console
.
error
(
'
Failed to load API keys:
'
,
error
)
}
finally
{
loading
.
value
=
false
}
}
</
script
>
\ No newline at end of file
</
script
>
frontend/src/components/admin/user/UserBalanceModal.vue
View file @
e7bc6250
...
...
@@ -29,10 +29,10 @@ import { reactive, ref, watch } from 'vue'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
User
}
from
'
@/types
'
import
type
{
Admin
User
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
const
props
=
defineProps
<
{
show
:
boolean
,
user
:
User
|
null
,
operation
:
'
add
'
|
'
subtract
'
}
>
()
const
props
=
defineProps
<
{
show
:
boolean
,
user
:
Admin
User
|
null
,
operation
:
'
add
'
|
'
subtract
'
}
>
()
const
emit
=
defineEmits
([
'
close
'
,
'
success
'
]);
const
{
t
}
=
useI18n
();
const
appStore
=
useAppStore
()
const
submitting
=
ref
(
false
);
const
form
=
reactive
({
amount
:
0
,
notes
:
''
})
...
...
frontend/src/components/admin/user/UserEditModal.vue
View file @
e7bc6250
...
...
@@ -56,12 +56,12 @@ import { useI18n } from 'vue-i18n'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
User
,
UserAttributeValuesMap
}
from
'
@/types
'
import
type
{
Admin
User
,
UserAttributeValuesMap
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
UserAttributeForm
from
'
@/components/user/UserAttributeForm.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
const
props
=
defineProps
<
{
show
:
boolean
,
user
:
User
|
null
}
>
()
const
props
=
defineProps
<
{
show
:
boolean
,
user
:
Admin
User
|
null
}
>
()
const
emit
=
defineEmits
([
'
close
'
,
'
success
'
])
const
{
t
}
=
useI18n
();
const
appStore
=
useAppStore
();
const
{
copyToClipboard
}
=
useClipboard
()
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment