mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-08 17:36:59 +08:00
* feat(i18n): translate web console messages in the frontend and AI-client errors in the backend
Chinese text was hard-coded in API messages, stored log content, emails and
server logs, so it reached users of every interface language.
Text is now translated where its reader is:
- Web console APIs return the English source text as message_key with
message_params, and the web console translates it in all seven languages.
message keeps the English text for other API callers.
- Errors returned to AI clients, emails and notifications are translated by
the backend (en, zh-CN, zh-TW) in the language saved on the account, then
the language of the request's Accept-Language header.
- A reader who states no language still gets the original Chinese text of the
messages that were Chinese before, word for word. Gateways that use another
new-api as their upstream match that text, for example to disable a channel
whose account ran out of quota.
- Usage, top-up and system log content is stored as content_parts (key and
params) and rendered in the viewer's language. Task fail reasons and refund
reasons written by the gateway are English keys. Rows written earlier are
shown as before.
- Server logs are English.
Also:
- The saved interface language zhTW, and zh-HK, zh-MO and zh-Hant, now select
Traditional Chinese in the backend instead of Simplified Chinese.
- The built-in group descriptions are translated; descriptions an
administrator saved are shown as written.
- .agents/rules/i18n.md records the conventions. Tests in i18n/ fail on a
non-English string literal, on a web console message without translations
and on a locale key without a constant.
* feat(i18n): print server logs in Chinese when LOG_LANGUAGE asks for it
Server log lines are English by default. With LOG_LANGUAGE=zh-CN, the lines
that were Chinese before print their original Chinese text.
common.LogText formats a log line from its English format, which is also the
key of its Chinese text in common/log_text.zh-CN.json. A line without an entry
prints in English. go vet checks the arguments of every call, and
TestServerLogTexts checks that each entry still matches a call and has the
same verbs as its key.
* docs: list LOG_LANGUAGE in the docker compose example
* feat(i18n): translate every server log line and add DEFAULT_LANGUAGE
Every line the gateway prints goes through common.LogText. With
DEFAULT_LANGUAGE=zh-CN, the lines that have a Chinese text in
common/log_text.zh-CN.json print it and the others print English.
DEFAULT_LANGUAGE replaces LOG_LANGUAGE. It also sets the language of
backend messages, emails and notifications for a reader who states no
language: en, zh-CN or zh-TW, English when unset. The list of messages
that stayed Chinese for such readers is removed.
Review fixes:
- Channel test logs store the token name "Model test"; the console shows
it in the interface language.
- Task fail reasons written by the gateway are fixed sentences the console
translates. The HTTP status and the upstream detail go to the server log
when a polled task is failed.
- The auto-disable reason of a slow channel is written in DEFAULT_LANGUAGE.
Also:
- Drop the repository-scanning lint tests, the i18n rules file and its
read gate. AGENTS.md keeps the conventions in four lines.
- Payment error toasts keep their previous text and only add the
translated message key.
- Remove guards and helpers that no caller needs.
* fix(i18n): address review on language lookup, notices and error responses
- Relay requests no longer load the user a second time to find the
language: the settings TokenAuth put into the request are used as they
are, also when they hold no language.
- Upstream model update notices are rendered in each recipient's saved
language, DEFAULT_LANGUAGE when none.
- /api/usage/token and /api/log/token answer with backend-translated
messages again, with the keys and status codes used before this PR.
Their callers are scripts, not the web console.
- Webhook notifications fill {{value}} placeholders like the other
senders. Running the rendered text through Sprintf appended
%!(EXTRA ...) and broke %2F in links.
- RootUserLanguage reads the setting of the root user it already loaded.
- zh_TW and zh_HK are read as Traditional Chinese.
- Add common.ApiErrorStatus for error responses under another HTTP status
and use it where those responses were built by hand.
- Remove middleware.GetLanguage, which had no callers.
550 lines
15 KiB
Go
550 lines
15 KiB
Go
package controller
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/i18n"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
type tokenAutoGroupsInput struct {
|
|
Set bool
|
|
Groups []string
|
|
}
|
|
|
|
func (input *tokenAutoGroupsInput) UnmarshalJSON(data []byte) error {
|
|
input.Set = true
|
|
if strings.TrimSpace(string(data)) == "null" {
|
|
input.Groups = nil
|
|
return nil
|
|
}
|
|
return common.Unmarshal(data, &input.Groups)
|
|
}
|
|
|
|
type tokenRequest struct {
|
|
model.Token
|
|
AutoGroups tokenAutoGroupsInput `json:"auto_groups"`
|
|
}
|
|
|
|
type tokenResponse struct {
|
|
*model.Token
|
|
AutoGroups []string `json:"auto_groups"`
|
|
}
|
|
|
|
func maxTokenQuota() int {
|
|
quota, err := common.WalletQuotaFromDecimalStrict(
|
|
decimal.NewFromInt(1_000_000_000).Mul(decimal.NewFromFloat(common.QuotaPerUnit)),
|
|
)
|
|
if err != nil {
|
|
return common.MaxWalletQuota
|
|
}
|
|
return quota
|
|
}
|
|
|
|
func buildMaskedTokenResponse(token *model.Token) *tokenResponse {
|
|
if token == nil {
|
|
return nil
|
|
}
|
|
maskedToken := *token
|
|
maskedToken.Key = token.GetMaskedKey()
|
|
autoGroups, err := token.GetAutoGroups()
|
|
if err != nil {
|
|
common.SysError(common.LogText("failed to parse auto groups for token %d: %v", token.Id, err))
|
|
autoGroups = nil
|
|
}
|
|
if len(autoGroups) == 0 {
|
|
autoGroups = nil
|
|
}
|
|
return &tokenResponse{Token: &maskedToken, AutoGroups: autoGroups}
|
|
}
|
|
|
|
func buildMaskedTokenResponses(tokens []*model.Token) []*tokenResponse {
|
|
maskedTokens := make([]*tokenResponse, 0, len(tokens))
|
|
for _, token := range tokens {
|
|
maskedTokens = append(maskedTokens, buildMaskedTokenResponse(token))
|
|
}
|
|
return maskedTokens
|
|
}
|
|
|
|
func getTokenRequestUserGroup(c *gin.Context) (string, error) {
|
|
if userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup); userGroup != "" {
|
|
return userGroup, nil
|
|
}
|
|
if userGroup := c.GetString("group"); userGroup != "" {
|
|
return userGroup, nil
|
|
}
|
|
return model.GetUserGroup(c.GetInt("id"), false)
|
|
}
|
|
|
|
func setTokenAutoGroups(c *gin.Context, token *model.Token, groups []string) bool {
|
|
if len(groups) == 0 {
|
|
if err := token.SetAutoGroups(nil); err != nil {
|
|
common.ApiError(c, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
maxCount := setting.GetMaxTokenAutoGroups()
|
|
if len(groups) > maxCount {
|
|
common.ApiErrorT(c, "A token can select at most {{max}} Auto groups", map[string]any{"max": maxCount})
|
|
return false
|
|
}
|
|
|
|
userGroup, err := getTokenRequestUserGroup(c)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return false
|
|
}
|
|
seen := make(map[string]struct{}, len(groups))
|
|
for _, group := range groups {
|
|
if _, ok := seen[group]; ok {
|
|
common.ApiErrorT(c, "Auto group {{group}} is duplicated", map[string]any{"group": group})
|
|
return false
|
|
}
|
|
seen[group] = struct{}{}
|
|
if !service.IsUserSelectableGroup(userGroup, group) {
|
|
common.ApiErrorT(c, "Auto group {{group}} is unavailable or unauthorized", map[string]any{"group": group})
|
|
return false
|
|
}
|
|
}
|
|
|
|
if err := token.SetAutoGroups(groups); err != nil {
|
|
common.ApiError(c, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func GetAllTokens(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
pageInfo := common.GetPageQuery(c)
|
|
tokens, err := model.GetAllUserTokens(userId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
total, _ := model.CountUserTokens(userId)
|
|
pageInfo.SetTotal(int(total))
|
|
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
|
|
common.ApiSuccess(c, pageInfo)
|
|
}
|
|
|
|
func SearchTokens(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
keyword := c.Query("keyword")
|
|
token := c.Query("token")
|
|
|
|
pageInfo := common.GetPageQuery(c)
|
|
|
|
tokens, total, err := model.SearchUserTokens(userId, keyword, token, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
pageInfo.SetTotal(int(total))
|
|
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
|
|
common.ApiSuccess(c, pageInfo)
|
|
}
|
|
|
|
func GetToken(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
userId := c.GetInt("id")
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
token, err := model.GetTokenByIds(id, userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, buildMaskedTokenResponse(token))
|
|
}
|
|
|
|
func GetTokenAutoGroups(c *gin.Context) {
|
|
userGroup, err := getTokenRequestUserGroup(c)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, gin.H{
|
|
"groups": service.GetUserAutoGroup(userGroup),
|
|
"max_count": setting.GetMaxTokenAutoGroups(),
|
|
})
|
|
}
|
|
|
|
func GetTokenKey(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
userId := c.GetInt("id")
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
token, err := model.GetTokenByIds(id, userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
params := tokenAuditParams(c)
|
|
params["id"], params["name"] = token.Id, token.Name
|
|
common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true)
|
|
common.ApiSuccess(c, gin.H{
|
|
"key": token.GetFullKey(),
|
|
})
|
|
}
|
|
|
|
func GetTokenStatus(c *gin.Context) {
|
|
tokenId := c.GetInt("token_id")
|
|
userId := c.GetInt("id")
|
|
token, err := model.GetTokenByIds(tokenId, userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
expiredAt := token.ExpiredTime
|
|
if expiredAt == -1 {
|
|
expiredAt = 0
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"object": "credit_summary",
|
|
"total_granted": token.RemainQuota,
|
|
"total_used": 0, // not supported currently
|
|
"total_available": token.RemainQuota,
|
|
"expires_at": expiredAt * 1000,
|
|
})
|
|
}
|
|
|
|
func GetTokenUsage(c *gin.Context) {
|
|
authHeader := c.GetHeader("Authorization")
|
|
if authHeader == "" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{
|
|
"success": false,
|
|
"message": "No Authorization header",
|
|
})
|
|
return
|
|
}
|
|
|
|
parts := strings.Split(authHeader, " ")
|
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{
|
|
"success": false,
|
|
"message": "Invalid Bearer token",
|
|
})
|
|
return
|
|
}
|
|
tokenKey := parts[1]
|
|
|
|
token, err := model.GetTokenByKey(strings.TrimPrefix(tokenKey, "sk-"), false)
|
|
if err != nil {
|
|
common.SysError(common.LogText("failed to get token by key: %s", err.Error()))
|
|
common.ApiErrorI18n(c, i18n.MsgTokenGetInfoFailed)
|
|
return
|
|
}
|
|
|
|
expiredAt := token.ExpiredTime
|
|
if expiredAt == -1 {
|
|
expiredAt = 0
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"code": true,
|
|
"message": "ok",
|
|
"data": gin.H{
|
|
"object": "token_usage",
|
|
"name": token.Name,
|
|
"total_granted": token.RemainQuota + token.UsedQuota,
|
|
"total_used": token.UsedQuota,
|
|
"total_available": token.RemainQuota,
|
|
"unlimited_quota": token.UnlimitedQuota,
|
|
"model_limits": token.GetModelLimitsMap(),
|
|
"model_limits_enabled": token.ModelLimitsEnabled,
|
|
"expires_at": expiredAt,
|
|
},
|
|
})
|
|
}
|
|
|
|
func AddToken(c *gin.Context) {
|
|
request := tokenRequest{}
|
|
err := c.ShouldBindJSON(&request)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
token := request.Token
|
|
if len(token.Name) > 50 {
|
|
common.ApiErrorT(c, "Token name is too long")
|
|
return
|
|
}
|
|
params := tokenAuditParams(c)
|
|
params["name"] = token.Name
|
|
// 非无限额度时,检查额度值是否超出有效范围
|
|
if !token.UnlimitedQuota {
|
|
if token.RemainQuota < 0 {
|
|
common.ApiErrorT(c, "Quota value cannot be negative")
|
|
return
|
|
}
|
|
maxQuotaValue := maxTokenQuota()
|
|
if token.RemainQuota > maxQuotaValue {
|
|
common.ApiErrorT(c, "Quota value exceeds valid range, maximum is {{max}}", map[string]any{"max": logger.FormatQuota(maxQuotaValue)})
|
|
return
|
|
}
|
|
}
|
|
// 检查用户令牌数量是否已达上限
|
|
maxTokens := operation_setting.GetMaxUserTokens()
|
|
count, err := model.CountUserTokens(c.GetInt("id"))
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if int(count) >= maxTokens {
|
|
common.ApiErrorT(c, "Maximum number of tokens reached ({{max}})", map[string]any{"max": maxTokens})
|
|
return
|
|
}
|
|
if token.Group == "auto" {
|
|
if !setTokenAutoGroups(c, &token, request.AutoGroups.Groups) {
|
|
return
|
|
}
|
|
} else {
|
|
token.CrossGroupRetry = false
|
|
_ = token.SetAutoGroups(nil)
|
|
}
|
|
key, err := common.GenerateKey()
|
|
if err != nil {
|
|
common.ApiErrorT(c, "Failed to generate token")
|
|
common.SysLog(common.LogText("failed to generate token key: %s", err.Error()))
|
|
return
|
|
}
|
|
cleanToken := model.Token{
|
|
UserId: c.GetInt("id"),
|
|
Name: token.Name,
|
|
Key: key,
|
|
CreatedTime: common.GetTimestamp(),
|
|
AccessedTime: common.GetTimestamp(),
|
|
ExpiredTime: token.ExpiredTime,
|
|
RemainQuota: token.RemainQuota,
|
|
UnlimitedQuota: token.UnlimitedQuota,
|
|
ModelLimitsEnabled: token.ModelLimitsEnabled,
|
|
ModelLimits: token.ModelLimits,
|
|
AllowIps: token.AllowIps,
|
|
Group: token.Group,
|
|
CrossGroupRetry: token.CrossGroupRetry,
|
|
AutoGroups: token.AutoGroups,
|
|
}
|
|
err = cleanToken.Insert()
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
params["id"] = cleanToken.Id
|
|
common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
})
|
|
}
|
|
|
|
func DeleteToken(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
userId := c.GetInt("id")
|
|
token, err := model.GetTokenByIds(id, userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
params := tokenAuditParams(c)
|
|
params["id"], params["name"] = token.Id, token.Name
|
|
err = token.Delete()
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
})
|
|
}
|
|
|
|
func UpdateToken(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
statusOnly := c.Query("status_only")
|
|
request := tokenRequest{}
|
|
err := c.ShouldBindJSON(&request)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
token := request.Token
|
|
params := tokenAuditParams(c)
|
|
if token.Id > 0 {
|
|
params["id"] = token.Id
|
|
}
|
|
if len(token.Name) > 50 {
|
|
common.ApiErrorT(c, "Token name is too long")
|
|
return
|
|
}
|
|
if !token.UnlimitedQuota {
|
|
if token.RemainQuota < 0 {
|
|
common.ApiErrorT(c, "Quota value cannot be negative")
|
|
return
|
|
}
|
|
maxQuotaValue := maxTokenQuota()
|
|
if token.RemainQuota > maxQuotaValue {
|
|
common.ApiErrorT(c, "Quota value exceeds valid range, maximum is {{max}}", map[string]any{"max": logger.FormatQuota(maxQuotaValue)})
|
|
return
|
|
}
|
|
}
|
|
cleanToken, err := model.GetTokenByIds(token.Id, userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
params["name"] = cleanToken.Name
|
|
previous := *cleanToken
|
|
if token.Status == common.TokenStatusEnabled {
|
|
if cleanToken.Status == common.TokenStatusExpired && cleanToken.ExpiredTime <= common.GetTimestamp() && cleanToken.ExpiredTime != -1 {
|
|
common.ApiErrorT(c, "Token has expired and cannot be enabled. Please modify the expiration time or set it to never expire")
|
|
return
|
|
}
|
|
if cleanToken.Status == common.TokenStatusExhausted && cleanToken.RemainQuota <= 0 && !cleanToken.UnlimitedQuota {
|
|
common.ApiErrorT(c, "Token quota is exhausted and cannot be enabled. Please modify the remaining quota or set it to unlimited")
|
|
return
|
|
}
|
|
}
|
|
if statusOnly != "" {
|
|
cleanToken.Status = token.Status
|
|
} else {
|
|
// If you add more fields, please also update token.Update()
|
|
cleanToken.Name = token.Name
|
|
cleanToken.ExpiredTime = token.ExpiredTime
|
|
cleanToken.RemainQuota = token.RemainQuota
|
|
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
|
cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled
|
|
cleanToken.ModelLimits = token.ModelLimits
|
|
cleanToken.AllowIps = token.AllowIps
|
|
cleanToken.Group = token.Group
|
|
cleanToken.CrossGroupRetry = token.CrossGroupRetry
|
|
if token.Group != "auto" {
|
|
cleanToken.CrossGroupRetry = false
|
|
_ = cleanToken.SetAutoGroups(nil)
|
|
} else if request.AutoGroups.Set {
|
|
if !setTokenAutoGroups(c, cleanToken, request.AutoGroups.Groups) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
err = cleanToken.Update()
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
params["name"] = cleanToken.Name
|
|
if statusOnly != "" {
|
|
params["from"], params["to"] = previous.Status, cleanToken.Status
|
|
} else {
|
|
changedFields := []string{}
|
|
for _, field := range []struct {
|
|
name string
|
|
changed bool
|
|
}{
|
|
{"name", previous.Name != cleanToken.Name},
|
|
{"expired_time", previous.ExpiredTime != cleanToken.ExpiredTime},
|
|
{"remain_quota", previous.RemainQuota != cleanToken.RemainQuota},
|
|
{"unlimited_quota", previous.UnlimitedQuota != cleanToken.UnlimitedQuota},
|
|
{"model_limits_enabled", previous.ModelLimitsEnabled != cleanToken.ModelLimitsEnabled},
|
|
{"model_limits", previous.ModelLimits != cleanToken.ModelLimits},
|
|
{"allow_ips", (previous.AllowIps == nil) != (cleanToken.AllowIps == nil) ||
|
|
(previous.AllowIps != nil && cleanToken.AllowIps != nil && *previous.AllowIps != *cleanToken.AllowIps)},
|
|
{"group", previous.Group != cleanToken.Group},
|
|
{"cross_group_retry", previous.CrossGroupRetry != cleanToken.CrossGroupRetry},
|
|
{"auto_groups", previous.AutoGroups != cleanToken.AutoGroups},
|
|
} {
|
|
if field.changed {
|
|
changedFields = append(changedFields, field.name)
|
|
}
|
|
}
|
|
params["changed_fields"] = changedFields
|
|
}
|
|
common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": buildMaskedTokenResponse(cleanToken),
|
|
})
|
|
}
|
|
|
|
type TokenBatch struct {
|
|
Ids []int `json:"ids"`
|
|
}
|
|
|
|
func DeleteTokenBatch(c *gin.Context) {
|
|
tokenBatch := TokenBatch{}
|
|
if err := c.ShouldBindJSON(&tokenBatch); err != nil {
|
|
common.ApiErrorT(c, "Invalid parameters")
|
|
return
|
|
}
|
|
params := tokenBatchAuditParams(c, tokenBatch.Ids)
|
|
if len(tokenBatch.Ids) == 0 {
|
|
common.ApiErrorT(c, "Invalid parameters")
|
|
return
|
|
}
|
|
userId := c.GetInt("id")
|
|
count, err := model.BatchDeleteTokens(tokenBatch.Ids, userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
params["count"] = count
|
|
common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": count,
|
|
})
|
|
}
|
|
|
|
func GetTokenKeysBatch(c *gin.Context) {
|
|
tokenBatch := TokenBatch{}
|
|
if err := c.ShouldBindJSON(&tokenBatch); err != nil {
|
|
common.ApiErrorT(c, "Invalid parameters")
|
|
return
|
|
}
|
|
params := tokenBatchAuditParams(c, tokenBatch.Ids)
|
|
if len(tokenBatch.Ids) == 0 {
|
|
common.ApiErrorT(c, "Invalid parameters")
|
|
return
|
|
}
|
|
if len(tokenBatch.Ids) > 100 {
|
|
common.ApiErrorT(c, "Too many items in batch request, maximum is {{max}}", map[string]any{"max": 100})
|
|
return
|
|
}
|
|
userId := c.GetInt("id")
|
|
tokens, err := model.GetTokenKeysByIds(tokenBatch.Ids, userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
keysMap := make(map[int]string)
|
|
returnedIDs := make([]int, 0, len(tokens))
|
|
for _, t := range tokens {
|
|
keysMap[t.Id] = t.GetFullKey()
|
|
returnedIDs = append(returnedIDs, t.Id)
|
|
}
|
|
params["count"] = len(tokens)
|
|
params["returned_ids"] = returnedIDs
|
|
common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true)
|
|
common.ApiSuccess(c, gin.H{"keys": keysMap})
|
|
}
|