671 lines
19 KiB
Go
671 lines
19 KiB
Go
package tenant
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/database/gdb"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gtime"
|
||
|
||
"github.com/qianfree/team-api/internal/consts"
|
||
"github.com/qianfree/team-api/internal/logic/billing"
|
||
lcommon "github.com/qianfree/team-api/internal/logic/common"
|
||
"github.com/qianfree/team-api/internal/logic/relay"
|
||
lrelay "github.com/qianfree/team-api/internal/logic/relay"
|
||
"github.com/qianfree/team-api/internal/middleware"
|
||
do "github.com/qianfree/team-api/internal/model/do"
|
||
|
||
v1 "github.com/qianfree/team-api/api/tenant/v1"
|
||
"github.com/qianfree/team-api/internal/dao"
|
||
uc "github.com/qianfree/team-api/internal/utility/crypto"
|
||
)
|
||
|
||
// ApiKeyList 列出 API Keys,支持按类型过滤
|
||
func (s *sTenant) ApiKeyList(ctx context.Context, req *v1.TenantApiKeyListReq) (*v1.TenantApiKeyListRes, error) {
|
||
tenantID := middleware.GetTenantID(ctx)
|
||
userID := middleware.GetUserID(ctx)
|
||
role := middleware.GetUserRole(ctx)
|
||
page, pageSize := lcommon.NormalizePagination(req.Page, req.PageSize)
|
||
|
||
// member 只能查看个人密钥
|
||
if role == "member" && req.KeyType == "project" {
|
||
return &v1.TenantApiKeyListRes{
|
||
List: []map[string]any{},
|
||
Total: 0,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
query := dao.ApiKeys.Ctx(ctx).Where("tenant_id", tenantID)
|
||
|
||
if req.KeyType == "project" {
|
||
query = query.Where("key_type", "project")
|
||
if req.ProjectID > 0 {
|
||
query = query.Where("project_id", req.ProjectID)
|
||
}
|
||
} else {
|
||
query = query.Where("user_id", userID).Where("key_type", "personal")
|
||
}
|
||
|
||
// 按有效性过滤(valid=仅可用 / invalid=仅失效),默认 all 不过滤
|
||
query = applyValidityFilter(query, req.Validity)
|
||
|
||
type keyRow struct {
|
||
Id int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
KeyPrefix string `json:"key_prefix"`
|
||
Scope string `json:"scope"`
|
||
Status string `json:"status"`
|
||
KeyType string `json:"key_type"`
|
||
ProjectId *int64 `json:"project_id"`
|
||
ExpiresAt *time.Time `json:"expires_at"`
|
||
RateLimitQps *int `json:"rate_limit_qps"`
|
||
RateLimitConcurrency *int `json:"rate_limit_concurrency"`
|
||
IpWhitelist []string `json:"ip_whitelist"`
|
||
TotalQuota *float64 `json:"total_quota"`
|
||
UsedQuota *float64 `json:"used_quota"`
|
||
CreatedAt *time.Time `json:"created_at"`
|
||
UpdatedAt *time.Time `json:"updated_at"`
|
||
}
|
||
|
||
var keys []keyRow
|
||
var err error
|
||
var total int
|
||
err = query.Fields("id, name, key_prefix, scope, status, key_type, project_id, expires_at, rate_limit_qps, rate_limit_concurrency, ip_whitelist, total_quota, used_quota, created_at, updated_at").
|
||
OrderDesc("created_at").
|
||
Page(page, pageSize).
|
||
ScanAndCount(&keys, &total, false)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if keys == nil {
|
||
keys = []keyRow{}
|
||
}
|
||
|
||
// 批量查询所有 key 的模型数量(修复 N+1)
|
||
keyIDs := make([]int64, 0, len(keys))
|
||
for _, k := range keys {
|
||
keyIDs = append(keyIDs, k.Id)
|
||
}
|
||
modelCountMap := make(map[int64]int, len(keys))
|
||
if len(keyIDs) > 0 {
|
||
type countRow struct {
|
||
ApiKeyId int64 `json:"api_key_id"`
|
||
Cnt int `json:"cnt"`
|
||
}
|
||
var counts []countRow
|
||
err = dao.ApiKeyModelScopes.Ctx(ctx).
|
||
Fields("api_key_id, COUNT(*) AS cnt").
|
||
WhereIn("api_key_id", keyIDs).
|
||
Group("api_key_id").
|
||
Scan(&counts)
|
||
if err == nil {
|
||
for _, c := range counts {
|
||
modelCountMap[c.ApiKeyId] = c.Cnt
|
||
}
|
||
}
|
||
}
|
||
|
||
list := make([]map[string]any, 0, len(keys))
|
||
for _, k := range keys {
|
||
list = append(list, map[string]any{
|
||
"id": k.Id,
|
||
"name": k.Name,
|
||
"key_prefix": k.KeyPrefix,
|
||
"scope": k.Scope,
|
||
"model_count": modelCountMap[k.Id],
|
||
"status": k.Status,
|
||
"key_type": k.KeyType,
|
||
"project_id": k.ProjectId,
|
||
"expires_at": k.ExpiresAt,
|
||
"rate_limit_qps": k.RateLimitQps,
|
||
"rate_limit_concurrency": k.RateLimitConcurrency,
|
||
"ip_whitelist": k.IpWhitelist,
|
||
"total_quota": k.TotalQuota,
|
||
"used_quota": k.UsedQuota,
|
||
"created_at": k.CreatedAt,
|
||
"updated_at": k.UpdatedAt,
|
||
})
|
||
}
|
||
|
||
return &v1.TenantApiKeyListRes{
|
||
List: list,
|
||
Total: total,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
// applyValidityFilter 按有效性过滤密钥查询:valid=仅可用(active 且未过期),
|
||
// invalid=仅失效(禁用/吊销或已过期)。过期密钥的 status 仍为 active,
|
||
// 仅凭状态过滤不出来,必须同时比较 expires_at;列表与导出共用此过滤口径
|
||
func applyValidityFilter(query *gdb.Model, validity string) *gdb.Model {
|
||
switch validity {
|
||
case "valid":
|
||
return query.Where("status", "active").
|
||
Where("(expires_at IS NULL OR expires_at > NOW())")
|
||
case "invalid":
|
||
return query.Where("(status != 'active' OR (expires_at IS NOT NULL AND expires_at <= NOW()))")
|
||
default:
|
||
return query
|
||
}
|
||
}
|
||
|
||
func normalizeIPWhitelist(items []string) []string {
|
||
if len(items) == 0 {
|
||
return []string{}
|
||
}
|
||
|
||
result := make([]string, 0, len(items))
|
||
seen := make(map[string]bool, len(items))
|
||
for _, item := range items {
|
||
item = strings.TrimSpace(item)
|
||
if item == "" || seen[item] {
|
||
continue
|
||
}
|
||
seen[item] = true
|
||
result = append(result, item)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// ApiKeyCreate 创建新的 API Key
|
||
func (s *sTenant) ApiKeyCreate(ctx context.Context, req *v1.TenantApiKeyCreateReq) (*v1.TenantApiKeyCreateRes, error) {
|
||
tenantID := middleware.GetTenantID(ctx)
|
||
userID := middleware.GetUserID(ctx)
|
||
role := middleware.GetUserRole(ctx)
|
||
|
||
keyType := req.KeyType
|
||
if keyType == "" {
|
||
keyType = "personal"
|
||
}
|
||
if keyType != "personal" && keyType != "project" {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "无效的密钥类型")
|
||
}
|
||
|
||
// 项目密钥权限检查
|
||
if keyType == "project" {
|
||
if role != "owner" && role != "admin" {
|
||
return nil, lcommon.NewBusinessError(consts.CodeProjectKeyForbidden, consts.MsgProjectKeyForbidden)
|
||
}
|
||
if req.ProjectID <= 0 {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "项目密钥必须指定关联项目")
|
||
}
|
||
// 验证项目存在且活跃
|
||
var project *struct {
|
||
Id int64 `json:"id"`
|
||
Status string `json:"status"`
|
||
}
|
||
err := dao.TntProjects.Ctx(ctx).
|
||
Where("id", req.ProjectID).
|
||
Where("tenant_id", tenantID).
|
||
Scan(&project)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if project == nil {
|
||
return nil, lcommon.NewBusinessError(consts.CodeProjectNotFound, consts.MsgProjectNotFound)
|
||
}
|
||
if project.Status != "active" {
|
||
return nil, lcommon.NewBusinessError(consts.CodeProjectNotActive, consts.MsgProjectNotActive)
|
||
}
|
||
}
|
||
|
||
rawKey, prefix, encryptedKey, err := relay.GenerateApiKey(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
data := do.ApiKeys{
|
||
TenantId: tenantID,
|
||
UserId: userID,
|
||
Name: req.Name,
|
||
EncryptedKey: encryptedKey,
|
||
KeyPrefix: prefix,
|
||
Scope: req.Scope,
|
||
KeyType: keyType,
|
||
Status: "active",
|
||
}
|
||
|
||
if keyType == "project" {
|
||
data.ProjectId = req.ProjectID
|
||
}
|
||
|
||
if req.ExpiresAt != nil {
|
||
data.ExpiresAt = req.ExpiresAt
|
||
}
|
||
if req.RateLimitQps != nil {
|
||
if *req.RateLimitQps < 0 {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "QPS 限制不能小于 0")
|
||
}
|
||
data.RateLimitQps = *req.RateLimitQps
|
||
}
|
||
if req.RateLimitConcurrency != nil {
|
||
if *req.RateLimitConcurrency < 0 {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "并发限制不能小于 0")
|
||
}
|
||
data.RateLimitConcurrency = *req.RateLimitConcurrency
|
||
}
|
||
if req.IpWhitelist != nil {
|
||
data.IpWhitelist = normalizeIPWhitelist(*req.IpWhitelist)
|
||
}
|
||
if req.TotalQuota != nil {
|
||
if *req.TotalQuota < 0 {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "总额度不能小于 0")
|
||
}
|
||
d := billing.NewFromFloat(*req.TotalQuota)
|
||
data.TotalQuota = &d
|
||
}
|
||
|
||
result, err := dao.ApiKeys.Ctx(ctx).Insert(data)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
keyID, err := result.LastInsertId()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 写入模型范围,检查每次插入的错误
|
||
for _, modelName := range req.ModelNames {
|
||
if modelName != "" {
|
||
if _, err := dao.ApiKeyModelScopes.Ctx(ctx).Insert(do.ApiKeyModelScopes{
|
||
ApiKeyId: keyID,
|
||
ModelName: modelName,
|
||
}); err != nil {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, fmt.Sprintf("写入模型范围 %s 失败", modelName))
|
||
}
|
||
}
|
||
}
|
||
|
||
return &v1.TenantApiKeyCreateRes{
|
||
Id: keyID,
|
||
Name: req.Name,
|
||
Key: rawKey,
|
||
KeyPrefix: prefix,
|
||
Scope: req.Scope,
|
||
KeyType: keyType,
|
||
}, nil
|
||
}
|
||
|
||
// ApiKeyDelete 禁用 API Key
|
||
func (s *sTenant) ApiKeyDelete(ctx context.Context, req *v1.TenantApiKeyDeleteReq) (*v1.TenantApiKeyDeleteRes, error) {
|
||
tenantID := middleware.GetTenantID(ctx)
|
||
userID := middleware.GetUserID(ctx)
|
||
role := middleware.GetUserRole(ctx)
|
||
keyID := req.Id
|
||
|
||
// 先查询密钥信息以判断类型
|
||
type keyInfo struct {
|
||
KeyType string `json:"key_type"`
|
||
KeyPrefix string `json:"key_prefix"`
|
||
}
|
||
var info *keyInfo
|
||
err := dao.ApiKeys.Ctx(ctx).
|
||
Where("id", keyID).
|
||
Where("tenant_id", tenantID).
|
||
Fields("key_type, key_prefix").
|
||
Scan(&info)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if info == nil {
|
||
return nil, lcommon.NewNotFoundError("API key")
|
||
}
|
||
|
||
query := dao.ApiKeys.Ctx(ctx).
|
||
Where("id", keyID).
|
||
Where("tenant_id", tenantID)
|
||
|
||
if info.KeyType == "project" {
|
||
if role != "owner" && role != "admin" {
|
||
return nil, lcommon.NewBusinessError(consts.CodeProjectKeyForbidden, consts.MsgProjectKeyForbidden)
|
||
}
|
||
} else {
|
||
query = query.Where("user_id", userID)
|
||
}
|
||
|
||
result, err := query.Data(do.ApiKeys{
|
||
Status: "disabled",
|
||
}).Update()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
rowsAffected, _ := result.RowsAffected()
|
||
if rowsAffected == 0 {
|
||
return nil, lcommon.NewNotFoundError("API key")
|
||
}
|
||
|
||
relay.InvalidateApiKey(ctx, info.KeyPrefix)
|
||
|
||
return &v1.TenantApiKeyDeleteRes{}, nil
|
||
}
|
||
|
||
// ApiKeyUpdate 更新 API Key 的可编辑字段
|
||
func (s *sTenant) ApiKeyUpdate(ctx context.Context, req *v1.TenantApiKeyUpdateReq) (*v1.TenantApiKeyUpdateRes, error) {
|
||
tenantID := middleware.GetTenantID(ctx)
|
||
userID := middleware.GetUserID(ctx)
|
||
role := middleware.GetUserRole(ctx)
|
||
keyID := req.Id
|
||
|
||
// 查询密钥信息
|
||
type keyInfo struct {
|
||
KeyType string `json:"key_type"`
|
||
KeyPrefix string `json:"key_prefix"`
|
||
Status string `json:"status"`
|
||
UsedQuota float64 `json:"used_quota"`
|
||
}
|
||
var info *keyInfo
|
||
err := dao.ApiKeys.Ctx(ctx).
|
||
Where("id", keyID).
|
||
Where("tenant_id", tenantID).
|
||
Fields("key_type, key_prefix, status, used_quota").
|
||
Scan(&info)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if info == nil {
|
||
return nil, lcommon.NewNotFoundError("API key")
|
||
}
|
||
|
||
// 权限检查
|
||
if info.KeyType == "project" {
|
||
if role != "owner" && role != "admin" {
|
||
return nil, lcommon.NewBusinessError(consts.CodeProjectKeyForbidden, consts.MsgProjectKeyForbidden)
|
||
}
|
||
} else {
|
||
count, err := dao.ApiKeys.Ctx(ctx).
|
||
Where("id", keyID).
|
||
Where("tenant_id", tenantID).
|
||
Where("user_id", userID).
|
||
Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if count == 0 {
|
||
return nil, lcommon.NewNotFoundError("API key")
|
||
}
|
||
}
|
||
|
||
// 使用 DO 对象构建更新数据
|
||
data := do.ApiKeys{}
|
||
hasUpdate := false
|
||
|
||
if req.Name != "" {
|
||
data.Name = req.Name
|
||
hasUpdate = true
|
||
}
|
||
if req.Scope != "" {
|
||
data.Scope = req.Scope
|
||
hasUpdate = true
|
||
}
|
||
if req.Status != "" {
|
||
if req.Status != "active" && req.Status != "disabled" {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "无效的状态值")
|
||
}
|
||
// 从 disabled 恢复为 active 时,检查是否已过期
|
||
if req.Status == "active" && info.Status == "disabled" {
|
||
type expireCheck struct {
|
||
ExpiresAt *gtime.Time `json:"expires_at"`
|
||
}
|
||
var ec *expireCheck
|
||
err := dao.ApiKeys.Ctx(ctx).
|
||
Where("id", keyID).
|
||
Fields("expires_at").
|
||
Scan(&ec)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if ec != nil && ec.ExpiresAt != nil && ec.ExpiresAt.Before(gtime.Now()) {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "密钥已过期,无法重新启用")
|
||
}
|
||
}
|
||
data.Status = req.Status
|
||
hasUpdate = true
|
||
}
|
||
if req.ExpiresAt != nil {
|
||
data.ExpiresAt = req.ExpiresAt
|
||
hasUpdate = true
|
||
}
|
||
if req.RateLimitQps != nil {
|
||
if *req.RateLimitQps < 0 {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "QPS 限制不能小于 0")
|
||
}
|
||
data.RateLimitQps = *req.RateLimitQps
|
||
hasUpdate = true
|
||
}
|
||
if req.RateLimitConcurrency != nil {
|
||
if *req.RateLimitConcurrency < 0 {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "并发限制不能小于 0")
|
||
}
|
||
data.RateLimitConcurrency = *req.RateLimitConcurrency
|
||
hasUpdate = true
|
||
}
|
||
if req.IpWhitelist != nil {
|
||
data.IpWhitelist = normalizeIPWhitelist(*req.IpWhitelist)
|
||
hasUpdate = true
|
||
}
|
||
if req.TotalQuota != nil {
|
||
if *req.TotalQuota < 0 {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "总额度不能小于 0")
|
||
}
|
||
if *req.TotalQuota > 0 && *req.TotalQuota < info.UsedQuota {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "总额度不能小于已用额度")
|
||
}
|
||
d := billing.NewFromFloat(*req.TotalQuota)
|
||
data.TotalQuota = &d
|
||
hasUpdate = true
|
||
}
|
||
|
||
// 主表更新与模型范围更新必须在同一事务内:前者成功后者失败会留下中间态(如 Key 已改名但
|
||
// scope 仍为旧值)。外层事务包住两步;updateApiKeyModelScopes 在携带事务的 ctx 下会被
|
||
// GoFrame 视为嵌套事务(savepoint),与主表更新共享同一事务。缓存失效放在事务提交后执行。
|
||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||
if hasUpdate {
|
||
_, err = dao.ApiKeys.Ctx(ctx).
|
||
Where("id", keyID).
|
||
Where("tenant_id", tenantID).
|
||
Data(data).
|
||
Update()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 更新模型范围(事务内先删后插,作为外层事务的 savepoint)
|
||
if req.ModelNames != nil {
|
||
if err = updateApiKeyModelScopes(ctx, keyID, req.ModelNames); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if hasUpdate || req.ModelNames != nil {
|
||
relay.InvalidateApiKey(ctx, info.KeyPrefix)
|
||
// 失效模型权限缓存(修复:更新模型范围后缓存未失效的问题)
|
||
lrelay.NewDataProvider().InvalidateApiKeyModelCache(ctx, keyID)
|
||
}
|
||
|
||
return &v1.TenantApiKeyUpdateRes{}, nil
|
||
}
|
||
|
||
// ApiKeyUpdateScopes 更新 API Key 的模型 scope
|
||
func (s *sTenant) ApiKeyUpdateScopes(ctx context.Context, req *v1.TenantApiKeyUpdateScopesReq) (*v1.TenantApiKeyUpdateScopesRes, error) {
|
||
tenantID := middleware.GetTenantID(ctx)
|
||
userID := middleware.GetUserID(ctx)
|
||
role := middleware.GetUserRole(ctx)
|
||
keyID := req.Id
|
||
|
||
// 先查询密钥信息以判断类型
|
||
type keyInfo struct {
|
||
KeyType string `json:"key_type"`
|
||
KeyPrefix string `json:"key_prefix"`
|
||
}
|
||
var info *keyInfo
|
||
err := dao.ApiKeys.Ctx(ctx).
|
||
Where("id", keyID).
|
||
Where("tenant_id", tenantID).
|
||
Fields("key_type, key_prefix").
|
||
Scan(&info)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if info == nil {
|
||
return nil, lcommon.NewNotFoundError("API key")
|
||
}
|
||
|
||
// 权限检查
|
||
if info.KeyType == "project" {
|
||
if role != "owner" && role != "admin" {
|
||
return nil, lcommon.NewBusinessError(consts.CodeProjectKeyForbidden, consts.MsgProjectKeyForbidden)
|
||
}
|
||
} else {
|
||
count, err := dao.ApiKeys.Ctx(ctx).
|
||
Where("id", keyID).
|
||
Where("tenant_id", tenantID).
|
||
Where("user_id", userID).
|
||
Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if count == 0 {
|
||
return nil, lcommon.NewNotFoundError("API key")
|
||
}
|
||
}
|
||
|
||
err = updateApiKeyModelScopes(ctx, keyID, req.ModelNames)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
relay.InvalidateApiKey(ctx, info.KeyPrefix)
|
||
// 失效模型权限缓存(修复:更新模型范围后缓存未失效的问题)
|
||
lrelay.NewDataProvider().InvalidateApiKeyModelCache(ctx, keyID)
|
||
|
||
return &v1.TenantApiKeyUpdateScopesRes{}, nil
|
||
}
|
||
|
||
// updateApiKeyModelScopes 在事务内更新模型范围(先删后插)
|
||
func updateApiKeyModelScopes(ctx context.Context, keyID int64, modelNames []string) error {
|
||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||
_, err := dao.ApiKeyModelScopes.Ctx(ctx).
|
||
Where("api_key_id", keyID).
|
||
Delete()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for _, modelName := range modelNames {
|
||
if modelName != "" {
|
||
if _, err := dao.ApiKeyModelScopes.Ctx(ctx).Insert(do.ApiKeyModelScopes{
|
||
ApiKeyId: keyID,
|
||
ModelName: modelName,
|
||
}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// ApiKeyModelScopes 查询 API Key 的模型范围
|
||
func (s *sTenant) ApiKeyModelScopes(ctx context.Context, req *v1.TenantApiKeyModelScopesReq) (*v1.TenantApiKeyModelScopesRes, error) {
|
||
tenantID := middleware.GetTenantID(ctx)
|
||
|
||
// 验证 key 属于该租户
|
||
count, err := dao.ApiKeys.Ctx(ctx).
|
||
Where("id", req.Id).
|
||
Where("tenant_id", tenantID).
|
||
Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if count == 0 {
|
||
return nil, lcommon.NewNotFoundError("API key")
|
||
}
|
||
|
||
type scopeRow struct {
|
||
ModelName string `json:"model_name"`
|
||
}
|
||
var rows []scopeRow
|
||
err = dao.ApiKeyModelScopes.Ctx(ctx).
|
||
Where("api_key_id", req.Id).
|
||
Fields("model_name").
|
||
Order("model_name").
|
||
Scan(&rows)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
names := make([]string, 0, len(rows))
|
||
for _, r := range rows {
|
||
names = append(names, r.ModelName)
|
||
}
|
||
|
||
return &v1.TenantApiKeyModelScopesRes{ModelNames: names}, nil
|
||
}
|
||
|
||
// ApiKeyReveal 获取 API Key 明文值(用于 Playground 等场景)
|
||
func (s *sTenant) ApiKeyReveal(ctx context.Context, req *v1.TenantApiKeyRevealReq) (*v1.TenantApiKeyRevealRes, error) {
|
||
tenantID := middleware.GetTenantID(ctx)
|
||
userID := middleware.GetUserID(ctx)
|
||
role := middleware.GetUserRole(ctx)
|
||
|
||
type keyInfo struct {
|
||
KeyType string `json:"key_type"`
|
||
KeyPrefix string `json:"key_prefix"`
|
||
Status string `json:"status"`
|
||
EncryptedKey string `json:"encrypted_key"`
|
||
UserId int64 `json:"user_id"`
|
||
}
|
||
var info *keyInfo
|
||
err := dao.ApiKeys.Ctx(ctx).
|
||
Where("id", req.Id).
|
||
Where("tenant_id", tenantID).
|
||
Fields("key_type, key_prefix, status, encrypted_key, user_id").
|
||
Scan(&info)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if info == nil {
|
||
return nil, lcommon.NewNotFoundError("API key")
|
||
}
|
||
|
||
// 权限校验
|
||
if info.KeyType == "project" {
|
||
if role != "owner" && role != "admin" {
|
||
return nil, lcommon.NewBusinessError(consts.CodeProjectKeyForbidden, consts.MsgProjectKeyForbidden)
|
||
}
|
||
} else {
|
||
// 个人密钥:只有本人可以查看
|
||
if info.UserId != userID {
|
||
return nil, lcommon.NewNotFoundError("API key")
|
||
}
|
||
}
|
||
|
||
// 解密
|
||
encKey := relay.GetEncryptionKey()
|
||
plainKey, err := uc.DecryptString(encKey, info.EncryptedKey)
|
||
if err != nil {
|
||
return nil, lcommon.NewBusinessError(consts.CodeBadRequest, "密钥解密失败")
|
||
}
|
||
|
||
// 查看明文属于敏感读取(Key 可长期调用计费接口),落敏感访问日志留痕
|
||
lcommon.LogSensitiveAccess(ctx, userID, "tenant", "api_key", req.Id, "reveal", "查看 API Key 明文")
|
||
|
||
return &v1.TenantApiKeyRevealRes{
|
||
Key: plainKey,
|
||
KeyPrefix: info.KeyPrefix,
|
||
Status: info.Status,
|
||
}, nil
|
||
}
|