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
ba6de4c4
Commit
ba6de4c4
authored
Mar 04, 2026
by
shaw
Browse files
feat: /keys页面支持表单筛选
parent
46ea9170
Changes
17
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/api_key_handler.go
View file @
ba6de4c4
...
...
@@ -4,6 +4,7 @@ package handler
import
(
"context"
"strconv"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
...
...
@@ -73,7 +74,23 @@ func (h *APIKeyHandler) List(c *gin.Context) {
page
,
pageSize
:=
response
.
ParsePagination
(
c
)
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
}
keys
,
result
,
err
:=
h
.
apiKeyService
.
List
(
c
.
Request
.
Context
(),
subject
.
UserID
,
params
)
// Parse filter parameters
var
filters
service
.
APIKeyListFilters
if
search
:=
strings
.
TrimSpace
(
c
.
Query
(
"search"
));
search
!=
""
{
if
len
(
search
)
>
100
{
search
=
search
[
:
100
]
}
filters
.
Search
=
search
}
filters
.
Status
=
c
.
Query
(
"status"
)
if
groupIDStr
:=
c
.
Query
(
"group_id"
);
groupIDStr
!=
""
{
gid
,
err
:=
strconv
.
ParseInt
(
groupIDStr
,
10
,
64
)
if
err
==
nil
{
filters
.
GroupID
=
&
gid
}
}
keys
,
result
,
err
:=
h
.
apiKeyService
.
List
(
c
.
Request
.
Context
(),
subject
.
UserID
,
params
,
filters
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
backend/internal/handler/sora_client_handler_test.go
View file @
ba6de4c4
...
...
@@ -996,7 +996,7 @@ func (r *stubAPIKeyRepoForHandler) GetByKeyForAuth(context.Context, string) (*se
}
func
(
r
*
stubAPIKeyRepoForHandler
)
Update
(
context
.
Context
,
*
service
.
APIKey
)
error
{
return
nil
}
func
(
r
*
stubAPIKeyRepoForHandler
)
Delete
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
r
*
stubAPIKeyRepoForHandler
)
ListByUserID
(
_
context
.
Context
,
_
int64
,
_
pagination
.
PaginationParams
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
r
*
stubAPIKeyRepoForHandler
)
ListByUserID
(
_
context
.
Context
,
_
int64
,
_
pagination
.
PaginationParams
,
_
service
.
APIKeyListFilters
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
return
nil
,
nil
,
nil
}
func
(
r
*
stubAPIKeyRepoForHandler
)
VerifyOwnership
(
context
.
Context
,
int64
,
[]
int64
)
([]
int64
,
error
)
{
...
...
backend/internal/repository/api_key_repo.go
View file @
ba6de4c4
...
...
@@ -281,9 +281,27 @@ func (r *apiKeyRepository) Delete(ctx context.Context, id int64) error {
return
nil
}
func
(
r
*
apiKeyRepository
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
r
*
apiKeyRepository
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
,
filters
service
.
APIKeyListFilters
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
q
:=
r
.
activeQuery
()
.
Where
(
apikey
.
UserIDEQ
(
userID
))
// Apply filters
if
filters
.
Search
!=
""
{
q
=
q
.
Where
(
apikey
.
Or
(
apikey
.
NameContainsFold
(
filters
.
Search
),
apikey
.
KeyContainsFold
(
filters
.
Search
),
))
}
if
filters
.
Status
!=
""
{
q
=
q
.
Where
(
apikey
.
StatusEQ
(
filters
.
Status
))
}
if
filters
.
GroupID
!=
nil
{
if
*
filters
.
GroupID
==
0
{
q
=
q
.
Where
(
apikey
.
GroupIDIsNil
())
}
else
{
q
=
q
.
Where
(
apikey
.
GroupIDEQ
(
*
filters
.
GroupID
))
}
}
total
,
err
:=
q
.
Count
(
ctx
)
if
err
!=
nil
{
return
nil
,
nil
,
err
...
...
backend/internal/repository/api_key_repo_integration_test.go
View file @
ba6de4c4
...
...
@@ -158,7 +158,7 @@ func (s *APIKeyRepoSuite) TestListByUserID() {
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-list-1"
,
"Key 1"
,
nil
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-list-2"
,
"Key 2"
,
nil
)
keys
,
page
,
err
:=
s
.
repo
.
ListByUserID
(
s
.
ctx
,
user
.
ID
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
})
keys
,
page
,
err
:=
s
.
repo
.
ListByUserID
(
s
.
ctx
,
user
.
ID
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
}
,
service
.
APIKeyListFilters
{}
)
s
.
Require
()
.
NoError
(
err
,
"ListByUserID"
)
s
.
Require
()
.
Len
(
keys
,
2
)
s
.
Require
()
.
Equal
(
int64
(
2
),
page
.
Total
)
...
...
@@ -170,7 +170,7 @@ func (s *APIKeyRepoSuite) TestListByUserID_Pagination() {
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-page-"
+
string
(
rune
(
'a'
+
i
)),
"Key"
,
nil
)
}
keys
,
page
,
err
:=
s
.
repo
.
ListByUserID
(
s
.
ctx
,
user
.
ID
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
2
})
keys
,
page
,
err
:=
s
.
repo
.
ListByUserID
(
s
.
ctx
,
user
.
ID
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
2
}
,
service
.
APIKeyListFilters
{}
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Len
(
keys
,
2
)
s
.
Require
()
.
Equal
(
int64
(
5
),
page
.
Total
)
...
...
@@ -314,7 +314,7 @@ func (s *APIKeyRepoSuite) TestCRUD_Search_ClearGroupID() {
s
.
Require
()
.
Equal
(
service
.
StatusDisabled
,
got2
.
Status
)
s
.
Require
()
.
Nil
(
got2
.
GroupID
)
keys
,
page
,
err
:=
s
.
repo
.
ListByUserID
(
s
.
ctx
,
user
.
ID
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
})
keys
,
page
,
err
:=
s
.
repo
.
ListByUserID
(
s
.
ctx
,
user
.
ID
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
}
,
service
.
APIKeyListFilters
{}
)
s
.
Require
()
.
NoError
(
err
,
"ListByUserID"
)
s
.
Require
()
.
Equal
(
int64
(
1
),
page
.
Total
)
s
.
Require
()
.
Len
(
keys
,
1
)
...
...
backend/internal/server/api_contract_test.go
View file @
ba6de4c4
...
...
@@ -1411,7 +1411,7 @@ func (r *stubApiKeyRepo) Delete(ctx context.Context, id int64) error {
return
nil
}
func
(
r
*
stubApiKeyRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
r
*
stubApiKeyRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
,
_
service
.
APIKeyListFilters
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
ids
:=
make
([]
int64
,
0
,
len
(
r
.
byID
))
for
id
:=
range
r
.
byID
{
if
r
.
byID
[
id
]
.
UserID
==
userID
{
...
...
backend/internal/server/middleware/api_key_auth_google_test.go
View file @
ba6de4c4
...
...
@@ -56,7 +56,7 @@ func (f fakeAPIKeyRepo) Update(ctx context.Context, key *service.APIKey) error {
func
(
f
fakeAPIKeyRepo
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
return
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeAPIKeyRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
f
fakeAPIKeyRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
,
_
service
.
APIKeyListFilters
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeAPIKeyRepo
)
VerifyOwnership
(
ctx
context
.
Context
,
userID
int64
,
apiKeyIDs
[]
int64
)
([]
int64
,
error
)
{
...
...
backend/internal/server/middleware/api_key_auth_test.go
View file @
ba6de4c4
...
...
@@ -537,7 +537,7 @@ func (r *stubApiKeyRepo) Delete(ctx context.Context, id int64) error {
return
errors
.
New
(
"not implemented"
)
}
func
(
r
*
stubApiKeyRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
r
*
stubApiKeyRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
,
_
service
.
APIKeyListFilters
)
([]
service
.
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
}
...
...
backend/internal/service/admin_service.go
View file @
ba6de4c4
...
...
@@ -745,7 +745,7 @@ func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64,
func
(
s
*
adminServiceImpl
)
GetUserAPIKeys
(
ctx
context
.
Context
,
userID
int64
,
page
,
pageSize
int
)
([]
APIKey
,
int64
,
error
)
{
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
}
keys
,
result
,
err
:=
s
.
apiKeyRepo
.
ListByUserID
(
ctx
,
userID
,
params
)
keys
,
result
,
err
:=
s
.
apiKeyRepo
.
ListByUserID
(
ctx
,
userID
,
params
,
APIKeyListFilters
{}
)
if
err
!=
nil
{
return
nil
,
0
,
err
}
...
...
backend/internal/service/admin_service_apikey_test.go
View file @
ba6de4c4
...
...
@@ -91,7 +91,7 @@ func (s *apiKeyRepoStubForGroupUpdate) GetByKeyForAuth(context.Context, string)
panic
(
"unexpected"
)
}
func
(
s
*
apiKeyRepoStubForGroupUpdate
)
Delete
(
context
.
Context
,
int64
)
error
{
panic
(
"unexpected"
)
}
func
(
s
*
apiKeyRepoStubForGroupUpdate
)
ListByUserID
(
context
.
Context
,
int64
,
pagination
.
PaginationParams
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
s
*
apiKeyRepoStubForGroupUpdate
)
ListByUserID
(
context
.
Context
,
int64
,
pagination
.
PaginationParams
,
APIKeyListFilters
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected"
)
}
func
(
s
*
apiKeyRepoStubForGroupUpdate
)
VerifyOwnership
(
context
.
Context
,
int64
,
[]
int64
)
([]
int64
,
error
)
{
...
...
backend/internal/service/api_key.go
View file @
ba6de4c4
...
...
@@ -97,3 +97,10 @@ func (k *APIKey) GetDaysUntilExpiry() int {
}
return
int
(
duration
.
Hours
()
/
24
)
}
// APIKeyListFilters holds optional filtering parameters for listing API keys.
type
APIKeyListFilters
struct
{
Search
string
Status
string
GroupID
*
int64
// nil=不筛选, 0=无分组, >0=指定分组
}
backend/internal/service/api_key_service.go
View file @
ba6de4c4
...
...
@@ -55,7 +55,7 @@ type APIKeyRepository interface {
Update
(
ctx
context
.
Context
,
key
*
APIKey
)
error
Delete
(
ctx
context
.
Context
,
id
int64
)
error
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
,
filters
APIKeyListFilters
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
VerifyOwnership
(
ctx
context
.
Context
,
userID
int64
,
apiKeyIDs
[]
int64
)
([]
int64
,
error
)
CountByUserID
(
ctx
context
.
Context
,
userID
int64
)
(
int64
,
error
)
ExistsByKey
(
ctx
context
.
Context
,
key
string
)
(
bool
,
error
)
...
...
@@ -392,8 +392,8 @@ func (s *APIKeyService) Create(ctx context.Context, userID int64, req CreateAPIK
}
// List 获取用户的API Key列表
func
(
s
*
APIKeyService
)
List
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
keys
,
pagination
,
err
:=
s
.
apiKeyRepo
.
ListByUserID
(
ctx
,
userID
,
params
)
func
(
s
*
APIKeyService
)
List
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
,
filters
APIKeyListFilters
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
keys
,
pagination
,
err
:=
s
.
apiKeyRepo
.
ListByUserID
(
ctx
,
userID
,
params
,
filters
)
if
err
!=
nil
{
return
nil
,
nil
,
fmt
.
Errorf
(
"list api keys: %w"
,
err
)
}
...
...
backend/internal/service/api_key_service_cache_test.go
View file @
ba6de4c4
...
...
@@ -53,7 +53,7 @@ func (s *authRepoStub) Delete(ctx context.Context, id int64) error {
panic
(
"unexpected Delete call"
)
}
func
(
s
*
authRepoStub
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
s
*
authRepoStub
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
,
filters
APIKeyListFilters
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected ListByUserID call"
)
}
...
...
backend/internal/service/api_key_service_delete_test.go
View file @
ba6de4c4
...
...
@@ -81,7 +81,7 @@ func (s *apiKeyRepoStub) Delete(ctx context.Context, id int64) error {
// 以下是接口要求实现但本测试不关心的方法
func
(
s
*
apiKeyRepoStub
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
s
*
apiKeyRepoStub
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
,
filters
APIKeyListFilters
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected ListByUserID call"
)
}
...
...
frontend/src/api/keys.ts
View file @
ba6de4c4
...
...
@@ -10,18 +10,20 @@ import type { ApiKey, CreateApiKeyRequest, UpdateApiKeyRequest, PaginatedRespons
* List all API keys for current user
* @param page - Page number (default: 1)
* @param pageSize - Items per page (default: 10)
* @param filters - Optional filter parameters
* @param options - Optional request options
* @returns Paginated list of API keys
*/
export
async
function
list
(
page
:
number
=
1
,
pageSize
:
number
=
10
,
filters
?:
{
search
?:
string
;
status
?:
string
;
group_id
?:
number
|
string
},
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
ApiKey
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
ApiKey
>>
(
'
/keys
'
,
{
params
:
{
page
,
page_size
:
pageSize
},
params
:
{
page
,
page_size
:
pageSize
,
...
filters
},
signal
:
options
?.
signal
})
return
data
...
...
frontend/src/i18n/locales/en.ts
View file @
ba6de4c4
...
...
@@ -444,6 +444,9 @@ export default {
keys
:
{
title
:
'
API Keys
'
,
description
:
'
Manage your API keys and access tokens
'
,
searchPlaceholder
:
'
Search name or key...
'
,
allGroups
:
'
All Groups
'
,
allStatus
:
'
All Status
'
,
createKey
:
'
Create API Key
'
,
editKey
:
'
Edit API Key
'
,
deleteKey
:
'
Delete API Key
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
ba6de4c4
...
...
@@ -445,6 +445,9 @@ export default {
keys
:
{
title
:
'
API 密钥
'
,
description
:
'
管理您的 API 密钥和访问令牌
'
,
searchPlaceholder
:
'
搜索名称或Key...
'
,
allGroups
:
'
全部分组
'
,
allStatus
:
'
全部状态
'
,
createKey
:
'
创建密钥
'
,
editKey
:
'
编辑密钥
'
,
deleteKey
:
'
删除密钥
'
,
...
...
frontend/src/views/user/KeysView.vue
View file @
ba6de4c4
<
template
>
<AppLayout>
<TablePageLayout>
<template
#filters
>
<div
class=
"flex flex-wrap items-center gap-3"
>
<SearchInput
v-model=
"filterSearch"
:placeholder=
"t('keys.searchPlaceholder')"
class=
"w-full sm:w-64"
@
search=
"onFilterChange"
/>
<Select
:model-value=
"filterGroupId"
class=
"w-40"
:options=
"groupFilterOptions"
@
update:model-value=
"onGroupFilterChange"
/>
<Select
:model-value=
"filterStatus"
class=
"w-40"
:options=
"statusFilterOptions"
@
update:model-value=
"onStatusFilterChange"
/>
</div>
</
template
>
<
template
#actions
>
<div
class=
"flex justify-end gap-3"
>
<button
...
...
@@ -985,6 +1008,7 @@ import TablePageLayout from '@/components/layout/TablePageLayout.vue'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
SearchInput
from
'
@/components/common/SearchInput.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
UseKeyModal
from
'
@/components/keys/UseKeyModal.vue
'
import
GroupBadge
from
'
@/components/common/GroupBadge.vue
'
...
...
@@ -1042,6 +1066,11 @@ const pagination = ref({
pages
:
0
})
// Filter state
const
filterSearch
=
ref
(
''
)
const
filterStatus
=
ref
(
''
)
const
filterGroupId
=
ref
<
string
|
number
>
(
''
)
const
showCreateModal
=
ref
(
false
)
const
showEditModal
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
)
...
...
@@ -1116,6 +1145,36 @@ const statusOptions = computed(() => [
{
value
:
'
inactive
'
,
label
:
t
(
'
common.inactive
'
)
}
])
// Filter dropdown options
const
groupFilterOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
keys.allGroups
'
)
},
{
value
:
0
,
label
:
t
(
'
keys.noGroup
'
)
},
...
groups
.
value
.
map
((
g
)
=>
({
value
:
g
.
id
,
label
:
g
.
name
}))
])
const
statusFilterOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
keys.allStatus
'
)
},
{
value
:
'
active
'
,
label
:
t
(
'
keys.status.active
'
)
},
{
value
:
'
inactive
'
,
label
:
t
(
'
keys.status.inactive
'
)
},
{
value
:
'
quota_exhausted
'
,
label
:
t
(
'
keys.status.quota_exhausted
'
)
},
{
value
:
'
expired
'
,
label
:
t
(
'
keys.status.expired
'
)
}
])
const
onFilterChange
=
()
=>
{
pagination
.
value
.
page
=
1
loadApiKeys
()
}
const
onGroupFilterChange
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
filterGroupId
.
value
=
value
as
string
|
number
onFilterChange
()
}
const
onStatusFilterChange
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
filterStatus
.
value
=
value
as
string
onFilterChange
()
}
// Convert groups to Select options format with rate multiplier and subscription type
const
groupOptions
=
computed
(()
=>
groups
.
value
.
map
((
group
)
=>
({
...
...
@@ -1157,7 +1216,13 @@ const loadApiKeys = async () => {
const
{
signal
}
=
controller
loading
.
value
=
true
try
{
const
response
=
await
keysAPI
.
list
(
pagination
.
value
.
page
,
pagination
.
value
.
page_size
,
{
// Build filters
const
filters
:
{
search
?:
string
;
status
?:
string
;
group_id
?:
number
|
string
}
=
{}
if
(
filterSearch
.
value
)
filters
.
search
=
filterSearch
.
value
if
(
filterStatus
.
value
)
filters
.
status
=
filterStatus
.
value
if
(
filterGroupId
.
value
!==
''
)
filters
.
group_id
=
filterGroupId
.
value
const
response
=
await
keysAPI
.
list
(
pagination
.
value
.
page
,
pagination
.
value
.
page_size
,
filters
,
{
signal
})
if
(
signal
.
aborted
)
return
...
...
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