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
58701239
Commit
58701239
authored
Dec 20, 2025
by
shaw
Browse files
feat: 支持创建管理员APIKEY
parent
adebd941
Changes
11
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/setting_handler.go
View file @
58701239
...
@@ -256,3 +256,43 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
...
@@ -256,3 +256,43 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"Test email sent successfully"
})
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"Test email sent successfully"
})
}
}
// GetAdminApiKey 获取管理员 API Key 状态
// GET /api/v1/admin/settings/admin-api-key
func
(
h
*
SettingHandler
)
GetAdminApiKey
(
c
*
gin
.
Context
)
{
maskedKey
,
exists
,
err
:=
h
.
settingService
.
GetAdminApiKeyStatus
(
c
.
Request
.
Context
())
if
err
!=
nil
{
response
.
InternalError
(
c
,
"Failed to get admin API key status: "
+
err
.
Error
())
return
}
response
.
Success
(
c
,
gin
.
H
{
"exists"
:
exists
,
"masked_key"
:
maskedKey
,
})
}
// RegenerateAdminApiKey 生成/重新生成管理员 API Key
// POST /api/v1/admin/settings/admin-api-key/regenerate
func
(
h
*
SettingHandler
)
RegenerateAdminApiKey
(
c
*
gin
.
Context
)
{
key
,
err
:=
h
.
settingService
.
GenerateAdminApiKey
(
c
.
Request
.
Context
())
if
err
!=
nil
{
response
.
InternalError
(
c
,
"Failed to generate admin API key: "
+
err
.
Error
())
return
}
response
.
Success
(
c
,
gin
.
H
{
"key"
:
key
,
// 完整 key 只在生成时返回一次
})
}
// DeleteAdminApiKey 删除管理员 API Key
// DELETE /api/v1/admin/settings/admin-api-key
func
(
h
*
SettingHandler
)
DeleteAdminApiKey
(
c
*
gin
.
Context
)
{
if
err
:=
h
.
settingService
.
DeleteAdminApiKey
(
c
.
Request
.
Context
());
err
!=
nil
{
response
.
InternalError
(
c
,
"Failed to delete admin API key: "
+
err
.
Error
())
return
}
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"Admin API key deleted"
})
}
backend/internal/middleware/admin_auth.go
0 → 100644
View file @
58701239
package
middleware
import
(
"context"
"crypto/subtle"
"strings"
"sub2api/internal/model"
"sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// AdminAuth 管理员认证中间件
// 支持两种认证方式(通过不同的 header 区分):
// 1. Admin API Key: x-api-key: <admin-api-key>
// 2. JWT Token: Authorization: Bearer <jwt-token> (需要管理员角色)
func
AdminAuth
(
authService
*
service
.
AuthService
,
userRepo
interface
{
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
model
.
User
,
error
)
GetFirstAdmin
(
ctx
context
.
Context
)
(
*
model
.
User
,
error
)
},
settingService
*
service
.
SettingService
,
)
gin
.
HandlerFunc
{
return
func
(
c
*
gin
.
Context
)
{
// 检查 x-api-key header(Admin API Key 认证)
apiKey
:=
c
.
GetHeader
(
"x-api-key"
)
if
apiKey
!=
""
{
if
!
validateAdminApiKey
(
c
,
apiKey
,
settingService
,
userRepo
)
{
return
}
c
.
Next
()
return
}
// 检查 Authorization header(JWT 认证)
authHeader
:=
c
.
GetHeader
(
"Authorization"
)
if
authHeader
!=
""
{
parts
:=
strings
.
SplitN
(
authHeader
,
" "
,
2
)
if
len
(
parts
)
==
2
&&
parts
[
0
]
==
"Bearer"
{
if
!
validateJWTForAdmin
(
c
,
parts
[
1
],
authService
,
userRepo
)
{
return
}
c
.
Next
()
return
}
}
// 无有效认证信息
AbortWithError
(
c
,
401
,
"UNAUTHORIZED"
,
"Authorization required"
)
}
}
// validateAdminApiKey 验证管理员 API Key
func
validateAdminApiKey
(
c
*
gin
.
Context
,
key
string
,
settingService
*
service
.
SettingService
,
userRepo
interface
{
GetFirstAdmin
(
ctx
context
.
Context
)
(
*
model
.
User
,
error
)
},
)
bool
{
storedKey
,
err
:=
settingService
.
GetAdminApiKey
(
c
.
Request
.
Context
())
if
err
!=
nil
{
AbortWithError
(
c
,
500
,
"INTERNAL_ERROR"
,
"Internal server error"
)
return
false
}
// 未配置或不匹配,统一返回相同错误(避免信息泄露)
if
storedKey
==
""
||
subtle
.
ConstantTimeCompare
([]
byte
(
key
),
[]
byte
(
storedKey
))
!=
1
{
AbortWithError
(
c
,
401
,
"INVALID_ADMIN_KEY"
,
"Invalid admin API key"
)
return
false
}
// 获取真实的管理员用户
admin
,
err
:=
userRepo
.
GetFirstAdmin
(
c
.
Request
.
Context
())
if
err
!=
nil
{
AbortWithError
(
c
,
500
,
"INTERNAL_ERROR"
,
"No admin user found"
)
return
false
}
c
.
Set
(
string
(
ContextKeyUser
),
admin
)
c
.
Set
(
"auth_method"
,
"admin_api_key"
)
return
true
}
// validateJWTForAdmin 验证 JWT 并检查管理员权限
func
validateJWTForAdmin
(
c
*
gin
.
Context
,
token
string
,
authService
*
service
.
AuthService
,
userRepo
interface
{
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
model
.
User
,
error
)
},
)
bool
{
// 验证 JWT token
claims
,
err
:=
authService
.
ValidateToken
(
token
)
if
err
!=
nil
{
if
err
==
service
.
ErrTokenExpired
{
AbortWithError
(
c
,
401
,
"TOKEN_EXPIRED"
,
"Token has expired"
)
return
false
}
AbortWithError
(
c
,
401
,
"INVALID_TOKEN"
,
"Invalid token"
)
return
false
}
// 从数据库获取用户
user
,
err
:=
userRepo
.
GetByID
(
c
.
Request
.
Context
(),
claims
.
UserID
)
if
err
!=
nil
{
AbortWithError
(
c
,
401
,
"USER_NOT_FOUND"
,
"User not found"
)
return
false
}
// 检查用户状态
if
!
user
.
IsActive
()
{
AbortWithError
(
c
,
401
,
"USER_INACTIVE"
,
"User account is not active"
)
return
false
}
// 检查管理员权限
if
user
.
Role
!=
model
.
RoleAdmin
{
AbortWithError
(
c
,
403
,
"FORBIDDEN"
,
"Admin access required"
)
return
false
}
c
.
Set
(
string
(
ContextKeyUser
),
user
)
c
.
Set
(
"auth_method"
,
"jwt"
)
return
true
}
backend/internal/model/setting.go
View file @
58701239
...
@@ -46,8 +46,14 @@ const (
...
@@ -46,8 +46,14 @@ const (
// 默认配置
// 默认配置
SettingKeyDefaultConcurrency
=
"default_concurrency"
// 新用户默认并发量
SettingKeyDefaultConcurrency
=
"default_concurrency"
// 新用户默认并发量
SettingKeyDefaultBalance
=
"default_balance"
// 新用户默认余额
SettingKeyDefaultBalance
=
"default_balance"
// 新用户默认余额
// 管理员 API Key
SettingKeyAdminApiKey
=
"admin_api_key"
// 全局管理员 API Key(用于外部系统集成)
)
)
// 管理员 API Key 前缀(与用户 sk- 前缀区分)
const
AdminApiKeyPrefix
=
"admin-"
// SystemSettings 系统设置结构体(用于API响应)
// SystemSettings 系统设置结构体(用于API响应)
type
SystemSettings
struct
{
type
SystemSettings
struct
{
// 注册设置
// 注册设置
...
...
backend/internal/repository/user_repo.go
View file @
58701239
...
@@ -128,3 +128,16 @@ func (r *UserRepository) RemoveGroupFromAllowedGroups(ctx context.Context, group
...
@@ -128,3 +128,16 @@ func (r *UserRepository) RemoveGroupFromAllowedGroups(ctx context.Context, group
Update
(
"allowed_groups"
,
gorm
.
Expr
(
"array_remove(allowed_groups, ?)"
,
groupID
))
Update
(
"allowed_groups"
,
gorm
.
Expr
(
"array_remove(allowed_groups, ?)"
,
groupID
))
return
result
.
RowsAffected
,
result
.
Error
return
result
.
RowsAffected
,
result
.
Error
}
}
// GetFirstAdmin 获取第一个管理员用户(用于 Admin API Key 认证)
func
(
r
*
UserRepository
)
GetFirstAdmin
(
ctx
context
.
Context
)
(
*
model
.
User
,
error
)
{
var
user
model
.
User
err
:=
r
.
db
.
WithContext
(
ctx
)
.
Where
(
"role = ? AND status = ?"
,
model
.
RoleAdmin
,
model
.
StatusActive
)
.
Order
(
"id ASC"
)
.
First
(
&
user
)
.
Error
if
err
!=
nil
{
return
nil
,
err
}
return
&
user
,
nil
}
backend/internal/server/router.go
View file @
58701239
...
@@ -132,7 +132,7 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep
...
@@ -132,7 +132,7 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep
// 管理员接口
// 管理员接口
admin
:=
v1
.
Group
(
"/admin"
)
admin
:=
v1
.
Group
(
"/admin"
)
admin
.
Use
(
middleware
.
JWT
Auth
(
s
.
Auth
,
repos
.
User
)
,
middleware
.
AdminOnly
(
))
admin
.
Use
(
middleware
.
Admin
Auth
(
s
.
Auth
,
repos
.
User
,
s
.
Setting
))
{
{
// 仪表盘
// 仪表盘
dashboard
:=
admin
.
Group
(
"/dashboard"
)
dashboard
:=
admin
.
Group
(
"/dashboard"
)
...
@@ -236,6 +236,10 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep
...
@@ -236,6 +236,10 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep
adminSettings
.
PUT
(
""
,
h
.
Admin
.
Setting
.
UpdateSettings
)
adminSettings
.
PUT
(
""
,
h
.
Admin
.
Setting
.
UpdateSettings
)
adminSettings
.
POST
(
"/test-smtp"
,
h
.
Admin
.
Setting
.
TestSmtpConnection
)
adminSettings
.
POST
(
"/test-smtp"
,
h
.
Admin
.
Setting
.
TestSmtpConnection
)
adminSettings
.
POST
(
"/send-test-email"
,
h
.
Admin
.
Setting
.
SendTestEmail
)
adminSettings
.
POST
(
"/send-test-email"
,
h
.
Admin
.
Setting
.
SendTestEmail
)
// Admin API Key 管理
adminSettings
.
GET
(
"/admin-api-key"
,
h
.
Admin
.
Setting
.
GetAdminApiKey
)
adminSettings
.
POST
(
"/admin-api-key/regenerate"
,
h
.
Admin
.
Setting
.
RegenerateAdminApiKey
)
adminSettings
.
DELETE
(
"/admin-api-key"
,
h
.
Admin
.
Setting
.
DeleteAdminApiKey
)
}
}
// 系统管理
// 系统管理
...
...
backend/internal/service/ports/user.go
View file @
58701239
...
@@ -11,6 +11,7 @@ type UserRepository interface {
...
@@ -11,6 +11,7 @@ type UserRepository interface {
Create
(
ctx
context
.
Context
,
user
*
model
.
User
)
error
Create
(
ctx
context
.
Context
,
user
*
model
.
User
)
error
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
model
.
User
,
error
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
model
.
User
,
error
)
GetByEmail
(
ctx
context
.
Context
,
email
string
)
(
*
model
.
User
,
error
)
GetByEmail
(
ctx
context
.
Context
,
email
string
)
(
*
model
.
User
,
error
)
GetFirstAdmin
(
ctx
context
.
Context
)
(
*
model
.
User
,
error
)
Update
(
ctx
context
.
Context
,
user
*
model
.
User
)
error
Update
(
ctx
context
.
Context
,
user
*
model
.
User
)
error
Delete
(
ctx
context
.
Context
,
id
int64
)
error
Delete
(
ctx
context
.
Context
,
id
int64
)
error
...
...
backend/internal/service/setting_service.go
View file @
58701239
...
@@ -2,6 +2,8 @@ package service
...
@@ -2,6 +2,8 @@ package service
import
(
import
(
"context"
"context"
"crypto/rand"
"encoding/hex"
"errors"
"errors"
"fmt"
"fmt"
"strconv"
"strconv"
...
@@ -262,3 +264,63 @@ func (s *SettingService) GetTurnstileSecretKey(ctx context.Context) string {
...
@@ -262,3 +264,63 @@ func (s *SettingService) GetTurnstileSecretKey(ctx context.Context) string {
}
}
return
value
return
value
}
}
// GenerateAdminApiKey 生成新的管理员 API Key
func
(
s
*
SettingService
)
GenerateAdminApiKey
(
ctx
context
.
Context
)
(
string
,
error
)
{
// 生成 32 字节随机数 = 64 位十六进制字符
bytes
:=
make
([]
byte
,
32
)
if
_
,
err
:=
rand
.
Read
(
bytes
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"generate random bytes: %w"
,
err
)
}
key
:=
model
.
AdminApiKeyPrefix
+
hex
.
EncodeToString
(
bytes
)
// 存储到 settings 表
if
err
:=
s
.
settingRepo
.
Set
(
ctx
,
model
.
SettingKeyAdminApiKey
,
key
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"save admin api key: %w"
,
err
)
}
return
key
,
nil
}
// GetAdminApiKeyStatus 获取管理员 API Key 状态
// 返回脱敏的 key、是否存在、错误
func
(
s
*
SettingService
)
GetAdminApiKeyStatus
(
ctx
context
.
Context
)
(
maskedKey
string
,
exists
bool
,
err
error
)
{
key
,
err
:=
s
.
settingRepo
.
GetValue
(
ctx
,
model
.
SettingKeyAdminApiKey
)
if
err
!=
nil
{
if
errors
.
Is
(
err
,
gorm
.
ErrRecordNotFound
)
{
return
""
,
false
,
nil
}
return
""
,
false
,
err
}
if
key
==
""
{
return
""
,
false
,
nil
}
// 脱敏:显示前 10 位和后 4 位
if
len
(
key
)
>
14
{
maskedKey
=
key
[
:
10
]
+
"..."
+
key
[
len
(
key
)
-
4
:
]
}
else
{
maskedKey
=
key
}
return
maskedKey
,
true
,
nil
}
// GetAdminApiKey 获取完整的管理员 API Key(仅供内部验证使用)
// 如果未配置返回空字符串和 nil 错误,只有数据库错误时才返回 error
func
(
s
*
SettingService
)
GetAdminApiKey
(
ctx
context
.
Context
)
(
string
,
error
)
{
key
,
err
:=
s
.
settingRepo
.
GetValue
(
ctx
,
model
.
SettingKeyAdminApiKey
)
if
err
!=
nil
{
if
errors
.
Is
(
err
,
gorm
.
ErrRecordNotFound
)
{
return
""
,
nil
// 未配置,返回空字符串
}
return
""
,
err
// 数据库错误
}
return
key
,
nil
}
// DeleteAdminApiKey 删除管理员 API Key
func
(
s
*
SettingService
)
DeleteAdminApiKey
(
ctx
context
.
Context
)
error
{
return
s
.
settingRepo
.
Delete
(
ctx
,
model
.
SettingKeyAdminApiKey
)
}
frontend/src/api/admin/settings.ts
View file @
58701239
...
@@ -99,11 +99,49 @@ export async function sendTestEmail(request: SendTestEmailRequest): Promise<{ me
...
@@ -99,11 +99,49 @@ export async function sendTestEmail(request: SendTestEmailRequest): Promise<{ me
return
data
;
return
data
;
}
}
/**
* Admin API Key status response
*/
export
interface
AdminApiKeyStatus
{
exists
:
boolean
;
masked_key
:
string
;
}
/**
* Get admin API key status
* @returns Status indicating if key exists and masked version
*/
export
async
function
getAdminApiKey
():
Promise
<
AdminApiKeyStatus
>
{
const
{
data
}
=
await
apiClient
.
get
<
AdminApiKeyStatus
>
(
'
/admin/settings/admin-api-key
'
);
return
data
;
}
/**
* Regenerate admin API key
* @returns The new full API key (only shown once)
*/
export
async
function
regenerateAdminApiKey
():
Promise
<
{
key
:
string
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
key
:
string
}
>
(
'
/admin/settings/admin-api-key/regenerate
'
);
return
data
;
}
/**
* Delete admin API key
* @returns Success message
*/
export
async
function
deleteAdminApiKey
():
Promise
<
{
message
:
string
}
>
{
const
{
data
}
=
await
apiClient
.
delete
<
{
message
:
string
}
>
(
'
/admin/settings/admin-api-key
'
);
return
data
;
}
export
const
settingsAPI
=
{
export
const
settingsAPI
=
{
getSettings
,
getSettings
,
updateSettings
,
updateSettings
,
testSmtpConnection
,
testSmtpConnection
,
sendTestEmail
,
sendTestEmail
,
getAdminApiKey
,
regenerateAdminApiKey
,
deleteAdminApiKey
,
};
};
export
default
settingsAPI
;
export
default
settingsAPI
;
frontend/src/i18n/locales/en.ts
View file @
58701239
...
@@ -992,6 +992,28 @@ export default {
...
@@ -992,6 +992,28 @@ export default {
sending
:
'
Sending...
'
,
sending
:
'
Sending...
'
,
enterRecipientHint
:
'
Please enter a recipient email address
'
,
enterRecipientHint
:
'
Please enter a recipient email address
'
,
},
},
adminApiKey
:
{
title
:
'
Admin API Key
'
,
description
:
'
Global API key for external system integration with full admin access
'
,
notConfigured
:
'
Admin API key not configured
'
,
configured
:
'
Admin API key is active
'
,
currentKey
:
'
Current Key
'
,
regenerate
:
'
Regenerate
'
,
regenerating
:
'
Regenerating...
'
,
delete
:
'
Delete
'
,
deleting
:
'
Deleting...
'
,
create
:
'
Create Key
'
,
creating
:
'
Creating...
'
,
regenerateConfirm
:
'
Are you sure? The current key will be immediately invalidated.
'
,
deleteConfirm
:
'
Are you sure you want to delete the admin API key? External integrations will stop working.
'
,
keyGenerated
:
'
New admin API key generated
'
,
keyDeleted
:
'
Admin API key deleted
'
,
copyKey
:
'
Copy Key
'
,
keyCopied
:
'
Key copied to clipboard
'
,
keyWarning
:
'
This key will only be shown once. Please copy it now.
'
,
securityWarning
:
'
Warning: This key provides full admin access. Keep it secure.
'
,
usage
:
'
Usage: Add to request header - x-api-key: <your-admin-api-key>
'
,
},
saveSettings
:
'
Save Settings
'
,
saveSettings
:
'
Save Settings
'
,
saving
:
'
Saving...
'
,
saving
:
'
Saving...
'
,
settingsSaved
:
'
Settings saved successfully
'
,
settingsSaved
:
'
Settings saved successfully
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
58701239
...
@@ -1171,6 +1171,28 @@ export default {
...
@@ -1171,6 +1171,28 @@ export default {
sending
:
'
发送中...
'
,
sending
:
'
发送中...
'
,
enterRecipientHint
:
'
请输入收件人邮箱地址
'
,
enterRecipientHint
:
'
请输入收件人邮箱地址
'
,
},
},
adminApiKey
:
{
title
:
'
管理员 API Key
'
,
description
:
'
用于外部系统集成的全局 API Key,拥有完整的管理员权限
'
,
notConfigured
:
'
尚未配置管理员 API Key
'
,
configured
:
'
管理员 API Key 已启用
'
,
currentKey
:
'
当前密钥
'
,
regenerate
:
'
重新生成
'
,
regenerating
:
'
生成中...
'
,
delete
:
'
删除
'
,
deleting
:
'
删除中...
'
,
create
:
'
创建密钥
'
,
creating
:
'
创建中...
'
,
regenerateConfirm
:
'
确定要重新生成吗?当前密钥将立即失效。
'
,
deleteConfirm
:
'
确定要删除管理员 API Key 吗?外部集成将停止工作。
'
,
keyGenerated
:
'
新的管理员 API Key 已生成
'
,
keyDeleted
:
'
管理员 API Key 已删除
'
,
copyKey
:
'
复制密钥
'
,
keyCopied
:
'
密钥已复制到剪贴板
'
,
keyWarning
:
'
此密钥仅显示一次,请立即复制保存。
'
,
securityWarning
:
'
警告:此密钥拥有完整的管理员权限,请妥善保管。
'
,
usage
:
'
使用方法:在请求头中添加 x-api-key: <your-admin-api-key>
'
,
},
saveSettings
:
'
保存设置
'
,
saveSettings
:
'
保存设置
'
,
saving
:
'
保存中...
'
,
saving
:
'
保存中...
'
,
settingsSaved
:
'
设置保存成功
'
,
settingsSaved
:
'
设置保存成功
'
,
...
...
frontend/src/views/admin/SettingsView.vue
View file @
58701239
...
@@ -8,6 +8,106 @@
...
@@ -8,6 +8,106 @@
<!-- Settings Form -->
<!-- Settings Form -->
<form
v-else
@
submit.prevent=
"saveSettings"
class=
"space-y-6"
>
<form
v-else
@
submit.prevent=
"saveSettings"
class=
"space-y-6"
>
<!-- Admin API Key Settings -->
<div
class=
"card"
>
<div
class=
"px-6 py-4 border-b border-gray-100 dark:border-dark-700"
>
<h2
class=
"text-lg font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
admin.settings.adminApiKey.title
'
)
}}
</h2>
<p
class=
"text-sm text-gray-500 dark:text-gray-400 mt-1"
>
{{
t
(
'
admin.settings.adminApiKey.description
'
)
}}
</p>
</div>
<div
class=
"p-6 space-y-4"
>
<!-- Security Warning -->
<div
class=
"bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4"
>
<div
class=
"flex items-start"
>
<svg
class=
"w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5"
fill=
"currentColor"
viewBox=
"0 0 20 20"
>
<path
fill-rule=
"evenodd"
d=
"M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule=
"evenodd"
/>
</svg>
<p
class=
"ml-3 text-sm text-amber-700 dark:text-amber-300"
>
{{
t
(
'
admin.settings.adminApiKey.securityWarning
'
)
}}
</p>
</div>
</div>
<!-- Loading State -->
<div
v-if=
"adminApiKeyLoading"
class=
"flex items-center gap-2 text-gray-500"
>
<div
class=
"animate-spin rounded-full h-4 w-4 border-b-2 border-primary-600"
></div>
{{
t
(
'
common.loading
'
)
}}
</div>
<!-- No Key Configured -->
<div
v-else-if=
"!adminApiKeyExists"
class=
"flex items-center justify-between"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.adminApiKey.notConfigured
'
)
}}
</span>
<button
type=
"button"
@
click=
"createAdminApiKey"
:disabled=
"adminApiKeyOperating"
class=
"btn btn-primary btn-sm"
>
<svg
v-if=
"adminApiKeyOperating"
class=
"animate-spin h-4 w-4 mr-1"
fill=
"none"
viewBox=
"0 0 24 24"
>
<circle
class=
"opacity-25"
cx=
"12"
cy=
"12"
r=
"10"
stroke=
"currentColor"
stroke-width=
"4"
></circle>
<path
class=
"opacity-75"
fill=
"currentColor"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{{
adminApiKeyOperating
?
t
(
'
admin.settings.adminApiKey.creating
'
)
:
t
(
'
admin.settings.adminApiKey.create
'
)
}}
</button>
</div>
<!-- Key Exists -->
<div
v-else
class=
"space-y-4"
>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
{{
t
(
'
admin.settings.adminApiKey.currentKey
'
)
}}
</label>
<code
class=
"text-sm font-mono text-gray-900 dark:text-gray-100 bg-gray-100 dark:bg-dark-700 px-2 py-1 rounded"
>
{{
adminApiKeyMasked
}}
</code>
</div>
<div
class=
"flex gap-2"
>
<button
type=
"button"
@
click=
"regenerateAdminApiKey"
:disabled=
"adminApiKeyOperating"
class=
"btn btn-secondary btn-sm"
>
{{
adminApiKeyOperating
?
t
(
'
admin.settings.adminApiKey.regenerating
'
)
:
t
(
'
admin.settings.adminApiKey.regenerate
'
)
}}
</button>
<button
type=
"button"
@
click=
"deleteAdminApiKey"
:disabled=
"adminApiKeyOperating"
class=
"btn btn-secondary btn-sm text-red-600 hover:text-red-700 dark:text-red-400"
>
{{
t
(
'
admin.settings.adminApiKey.delete
'
)
}}
</button>
</div>
</div>
<!-- Newly Generated Key Display -->
<div
v-if=
"newAdminApiKey"
class=
"bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 space-y-3"
>
<p
class=
"text-sm font-medium text-green-700 dark:text-green-300"
>
{{
t
(
'
admin.settings.adminApiKey.keyWarning
'
)
}}
</p>
<div
class=
"flex items-center gap-2"
>
<code
class=
"flex-1 text-sm font-mono bg-white dark:bg-dark-800 px-3 py-2 rounded border border-green-300 dark:border-green-700 break-all select-all"
>
{{
newAdminApiKey
}}
</code>
<button
type=
"button"
@
click=
"copyNewKey"
class=
"btn btn-primary btn-sm flex-shrink-0"
>
{{
t
(
'
admin.settings.adminApiKey.copyKey
'
)
}}
</button>
</div>
<p
class=
"text-xs text-green-600 dark:text-green-400"
>
{{
t
(
'
admin.settings.adminApiKey.usage
'
)
}}
</p>
</div>
</div>
</div>
</div>
<!-- Registration Settings -->
<!-- Registration Settings -->
<div
class=
"card"
>
<div
class=
"card"
>
<div
class=
"px-6 py-4 border-b border-gray-100 dark:border-dark-700"
>
<div
class=
"px-6 py-4 border-b border-gray-100 dark:border-dark-700"
>
...
@@ -424,6 +524,13 @@ const sendingTestEmail = ref(false);
...
@@ -424,6 +524,13 @@ const sendingTestEmail = ref(false);
const
testEmailAddress
=
ref
(
''
);
const
testEmailAddress
=
ref
(
''
);
const
logoError
=
ref
(
''
);
const
logoError
=
ref
(
''
);
// Admin API Key 状态
const
adminApiKeyLoading
=
ref
(
true
);
const
adminApiKeyExists
=
ref
(
false
);
const
adminApiKeyMasked
=
ref
(
''
);
const
adminApiKeyOperating
=
ref
(
false
);
const
newAdminApiKey
=
ref
(
''
);
const
form
=
reactive
<
SystemSettings
>
({
const
form
=
reactive
<
SystemSettings
>
({
registration_enabled
:
true
,
registration_enabled
:
true
,
email_verify_enabled
:
false
,
email_verify_enabled
:
false
,
...
@@ -555,7 +662,66 @@ async function sendTestEmail() {
...
@@ -555,7 +662,66 @@ async function sendTestEmail() {
}
}
}
}
// Admin API Key 方法
async
function
loadAdminApiKey
()
{
adminApiKeyLoading
.
value
=
true
;
try
{
const
status
=
await
adminAPI
.
settings
.
getAdminApiKey
();
adminApiKeyExists
.
value
=
status
.
exists
;
adminApiKeyMasked
.
value
=
status
.
masked_key
;
}
catch
(
error
:
any
)
{
console
.
error
(
'
Failed to load admin API key status:
'
,
error
);
}
finally
{
adminApiKeyLoading
.
value
=
false
;
}
}
async
function
createAdminApiKey
()
{
adminApiKeyOperating
.
value
=
true
;
try
{
const
result
=
await
adminAPI
.
settings
.
regenerateAdminApiKey
();
newAdminApiKey
.
value
=
result
.
key
;
adminApiKeyExists
.
value
=
true
;
adminApiKeyMasked
.
value
=
result
.
key
.
substring
(
0
,
10
)
+
'
...
'
+
result
.
key
.
slice
(
-
4
);
appStore
.
showSuccess
(
t
(
'
admin.settings.adminApiKey.keyGenerated
'
));
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
message
||
t
(
'
common.error
'
));
}
finally
{
adminApiKeyOperating
.
value
=
false
;
}
}
async
function
regenerateAdminApiKey
()
{
if
(
!
confirm
(
t
(
'
admin.settings.adminApiKey.regenerateConfirm
'
)))
return
;
await
createAdminApiKey
();
}
async
function
deleteAdminApiKey
()
{
if
(
!
confirm
(
t
(
'
admin.settings.adminApiKey.deleteConfirm
'
)))
return
;
adminApiKeyOperating
.
value
=
true
;
try
{
await
adminAPI
.
settings
.
deleteAdminApiKey
();
adminApiKeyExists
.
value
=
false
;
adminApiKeyMasked
.
value
=
''
;
newAdminApiKey
.
value
=
''
;
appStore
.
showSuccess
(
t
(
'
admin.settings.adminApiKey.keyDeleted
'
));
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
message
||
t
(
'
common.error
'
));
}
finally
{
adminApiKeyOperating
.
value
=
false
;
}
}
function
copyNewKey
()
{
navigator
.
clipboard
.
writeText
(
newAdminApiKey
.
value
).
then
(()
=>
{
appStore
.
showSuccess
(
t
(
'
admin.settings.adminApiKey.keyCopied
'
));
}).
catch
(()
=>
{
appStore
.
showError
(
t
(
'
common.copyFailed
'
));
});
}
onMounted
(()
=>
{
onMounted
(()
=>
{
loadSettings
();
loadSettings
();
loadAdminApiKey
();
});
});
</
script
>
</
script
>
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