mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-29 16:05:34 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1a3827f18 |
+144
-2
@@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func bindAPIKeyRequest(c *gin.Context, value interface{}) bool {
|
func bindAPIKeyRequest(c *gin.Context, value interface{}) bool {
|
||||||
if _, err := auth.RequireAPIKeySession(c); err != nil {
|
if _, err := auth.RequireAPIKeyManagement(c); err != nil {
|
||||||
helper.BadAuth(c, "ErrNotLogin", nil)
|
helper.BadAuth(c, "ErrNotLogin", nil)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,24 @@ func apiKeyError(c *gin.Context, err error) {
|
|||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swag's legacy || syntax combines schemes in one requirement (AND);
|
||||||
|
// separate @Security lines represent alternative authentication methods.
|
||||||
|
// @Tags API Key
|
||||||
|
// @Summary List the current account's API keys
|
||||||
|
// @Description Accepts a panel login session with CSRF protection, or a valid API Key signature. The authenticated account determines ownership; another account cannot be selected in the request.
|
||||||
|
// @Description Returns key metadata and masked hints, never plaintext secrets. The legacy key is included when present; deleted keys are excluded.
|
||||||
|
// @Description Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500; check code rather than HTTP status alone.
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.APIKeySearch true "Pagination and revoked-key filter"
|
||||||
|
// @Param 1Panel-Key-ID header string false "Optional ID of the authenticating independent key; omit for legacy keys"
|
||||||
|
// @Param 1Panel-Signature-Version header string false "Signature algorithm; omission accepts either supported algorithm" Enums(v1,md5,hmac-sha256)
|
||||||
|
// @Success 200 {object} dto.Response{data=dto.APIKeyPage} "code=200 on success; data is null on errors"
|
||||||
|
// @Failure 403 {object} dto.Response "Invalid or missing CSRF token for a session request"
|
||||||
|
// @Header 200 {string} Cache-Control "no-store"
|
||||||
|
// @Security ApiKeyAuth || Timestamp
|
||||||
|
// @Security PanelSession || CSRFToken
|
||||||
|
// @Router /core/auth/api/keys/search [post]
|
||||||
func (b *BaseApi) SearchAPIKeys(c *gin.Context) {
|
func (b *BaseApi) SearchAPIKeys(c *gin.Context) {
|
||||||
var req dto.APIKeySearch
|
var req dto.APIKeySearch
|
||||||
if !bindAPIKeyRequest(c, &req) {
|
if !bindAPIKeyRequest(c, &req) {
|
||||||
@@ -55,6 +73,25 @@ func (b *BaseApi) SearchAPIKeys(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, result)
|
helper.SuccessWithData(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags API Key
|
||||||
|
// @Summary Create an API key for the current account
|
||||||
|
// @Description Accepts a panel login session with CSRF protection, or a valid API Key signature. The new key belongs to the authenticated account.
|
||||||
|
// @Description requestID is optional. Omitted or empty values are generated by the server. For idempotent retries, supply a UUID and reuse it with the same payload.
|
||||||
|
// @Description A successful first request returns the plaintext secret. Repeating the same requestID and payload returns alreadyCreated=true and an empty apiKey; changing the payload for that requestID is rejected.
|
||||||
|
// @Description Omitted expiresAt defaults to 90 days; explicit null means no expiration. Omitted apiKeyValidityTime defaults to 120 minutes; zero disables timestamp validation.
|
||||||
|
// @Description API-created keys may have a longer lifetime or broader IP policy than the calling key. Revoking the calling key does not revoke keys it created.
|
||||||
|
// @Description Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500; check code rather than HTTP status alone. Unknown fields and bodies larger than 32 KiB are rejected.
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.APIKeyCreate true "Key policy and an optional UUID requestID for idempotency"
|
||||||
|
// @Param 1Panel-Key-ID header string false "Optional ID of the authenticating independent key; omit for legacy keys"
|
||||||
|
// @Param 1Panel-Signature-Version header string false "Signature algorithm; omission accepts either supported algorithm" Enums(v1,md5,hmac-sha256)
|
||||||
|
// @Success 200 {object} dto.Response{data=dto.APIKeyCreated} "code=200 on success; data is null on errors"
|
||||||
|
// @Failure 403 {object} dto.Response "Invalid or missing CSRF token for a session request"
|
||||||
|
// @Header 200 {string} Cache-Control "no-store"
|
||||||
|
// @Security ApiKeyAuth || Timestamp
|
||||||
|
// @Security PanelSession || CSRFToken
|
||||||
|
// @Router /core/auth/api/keys/create [post]
|
||||||
func (b *BaseApi) CreateAPIKey(c *gin.Context) {
|
func (b *BaseApi) CreateAPIKey(c *gin.Context) {
|
||||||
req := dto.APIKeyCreate{APIKeyFields: dto.APIKeyFields{APIKeyValidityTime: 120}}
|
req := dto.APIKeyCreate{APIKeyFields: dto.APIKeyFields{APIKeyValidityTime: 120}}
|
||||||
if !bindAPIKeyRequest(c, &req) {
|
if !bindAPIKeyRequest(c, &req) {
|
||||||
@@ -69,6 +106,22 @@ func (b *BaseApi) CreateAPIKey(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, result)
|
helper.SuccessWithData(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags API Key
|
||||||
|
// @Summary Update an API key's policy
|
||||||
|
// @Description Accepts a panel login session with CSRF protection, or a valid API Key signature. Only keys owned by the authenticated account can be changed.
|
||||||
|
// @Description Submit the current revision returned by search; stale revisions are rejected. This replaces editable policy fields without changing the secret. expiresAt must be supplied as an RFC 3339 timestamp or null.
|
||||||
|
// @Description Use id=legacy for the legacy key; its name is fixed and expiresAt must be null. Revoked or deleted independent keys cannot be updated.
|
||||||
|
// @Description Success increments the revision; search again before another mutation. Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500.
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.APIKeyUpdate true "Key ID, current revision and complete editable policy"
|
||||||
|
// @Param 1Panel-Key-ID header string false "Optional ID of the authenticating independent key; omit for legacy keys"
|
||||||
|
// @Param 1Panel-Signature-Version header string false "Signature algorithm; omission accepts either supported algorithm" Enums(v1,md5,hmac-sha256)
|
||||||
|
// @Success 200 {object} dto.Response{data=dto.APIKeyMutationResult} "code=200 on success; data is null on errors"
|
||||||
|
// @Failure 403 {object} dto.Response "Invalid or missing CSRF token for a session request"
|
||||||
|
// @Security ApiKeyAuth || Timestamp
|
||||||
|
// @Security PanelSession || CSRFToken
|
||||||
|
// @Router /core/auth/api/keys/update [post]
|
||||||
func (b *BaseApi) UpdateAPIKey(c *gin.Context) {
|
func (b *BaseApi) UpdateAPIKey(c *gin.Context) {
|
||||||
var req dto.APIKeyUpdate
|
var req dto.APIKeyUpdate
|
||||||
if !bindAPIKeyRequest(c, &req) {
|
if !bindAPIKeyRequest(c, &req) {
|
||||||
@@ -81,6 +134,21 @@ func (b *BaseApi) UpdateAPIKey(c *gin.Context) {
|
|||||||
apiKeyMutationSuccess(c)
|
apiKeyMutationSuccess(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags API Key
|
||||||
|
// @Summary Enable or disable an API key
|
||||||
|
// @Description Accepts a panel login session with CSRF protection, or a valid API Key signature. Only keys owned by the authenticated account can be changed; id=legacy is supported.
|
||||||
|
// @Description Submit the current revision returned by search. Revoked or deleted keys cannot be enabled; expired keys must first receive a future expiration time. Disabling a key also closes its active terminal sessions.
|
||||||
|
// @Description Success increments the revision; search again before another mutation. Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500.
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.APIKeyStatus true "Key ID, current revision and Enable or Disable status"
|
||||||
|
// @Param 1Panel-Key-ID header string false "Optional ID of the authenticating independent key; omit for legacy keys"
|
||||||
|
// @Param 1Panel-Signature-Version header string false "Signature algorithm; omission accepts either supported algorithm" Enums(v1,md5,hmac-sha256)
|
||||||
|
// @Success 200 {object} dto.Response{data=dto.APIKeyMutationResult} "code=200 on success; data is null on errors"
|
||||||
|
// @Failure 403 {object} dto.Response "Invalid or missing CSRF token for a session request"
|
||||||
|
// @Security ApiKeyAuth || Timestamp
|
||||||
|
// @Security PanelSession || CSRFToken
|
||||||
|
// @Router /core/auth/api/keys/status [post]
|
||||||
func (b *BaseApi) SetAPIKeyStatus(c *gin.Context) {
|
func (b *BaseApi) SetAPIKeyStatus(c *gin.Context) {
|
||||||
var req dto.APIKeyStatus
|
var req dto.APIKeyStatus
|
||||||
if !bindAPIKeyRequest(c, &req) {
|
if !bindAPIKeyRequest(c, &req) {
|
||||||
@@ -93,6 +161,22 @@ func (b *BaseApi) SetAPIKeyStatus(c *gin.Context) {
|
|||||||
apiKeyMutationSuccess(c)
|
apiKeyMutationSuccess(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags API Key
|
||||||
|
// @Summary Revoke an API key
|
||||||
|
// @Description Accepts a panel login session with CSRF protection, or a valid API Key signature. Only keys owned by the authenticated account can be revoked.
|
||||||
|
// @Description Submit the current revision returned by search. Revocation destroys the independent key's stored secret and cannot be undone; its metadata remains listed. For id=legacy, the legacy secret is cleared and API access is disabled.
|
||||||
|
// @Description A key can revoke itself; subsequent requests signed with it fail. Keys previously created by the revoked key remain independent. Success increments the revision and closes associated terminal sessions.
|
||||||
|
// @Description Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500.
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.APIKeyMutation true "Key ID and current revision"
|
||||||
|
// @Param 1Panel-Key-ID header string false "Optional ID of the authenticating independent key; omit for legacy keys"
|
||||||
|
// @Param 1Panel-Signature-Version header string false "Signature algorithm; omission accepts either supported algorithm" Enums(v1,md5,hmac-sha256)
|
||||||
|
// @Success 200 {object} dto.Response{data=dto.APIKeyMutationResult} "code=200 on success; data is null on errors"
|
||||||
|
// @Failure 403 {object} dto.Response "Invalid or missing CSRF token for a session request"
|
||||||
|
// @Security ApiKeyAuth || Timestamp
|
||||||
|
// @Security PanelSession || CSRFToken
|
||||||
|
// @Router /core/auth/api/keys/revoke [post]
|
||||||
func (b *BaseApi) RevokeAPIKey(c *gin.Context) {
|
func (b *BaseApi) RevokeAPIKey(c *gin.Context) {
|
||||||
var req dto.APIKeyMutation
|
var req dto.APIKeyMutation
|
||||||
if !bindAPIKeyRequest(c, &req) {
|
if !bindAPIKeyRequest(c, &req) {
|
||||||
@@ -106,5 +190,63 @@ func (b *BaseApi) RevokeAPIKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func apiKeyMutationSuccess(c *gin.Context) {
|
func apiKeyMutationSuccess(c *gin.Context) {
|
||||||
helper.SuccessWithData(c, gin.H{"terminalClosePending": c.GetBool("API_KEY_TERMINAL_CLOSE_PENDING")})
|
helper.SuccessWithData(c, dto.APIKeyMutationResult{TerminalClosePending: c.GetBool("API_KEY_TERMINAL_CLOSE_PENDING")})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags API Key
|
||||||
|
// @Summary Delete an independent API key
|
||||||
|
// @Description Accepts a panel login session with CSRF protection, or a valid API Key signature. Only keys owned by the authenticated account can be deleted; id=legacy is not supported.
|
||||||
|
// @Description Submit the current revision returned by search. Deletion immediately revokes the key, destroys its stored secret and removes it from lists. Audit and idempotency records are retained. Already-revoked keys can be deleted; deletion cannot be undone.
|
||||||
|
// @Description A key can delete itself; subsequent requests signed with it fail. Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500.
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.APIKeyMutation true "Independent key ID and current revision"
|
||||||
|
// @Param 1Panel-Key-ID header string false "Optional ID of the authenticating independent key; omit for legacy keys"
|
||||||
|
// @Param 1Panel-Signature-Version header string false "Signature algorithm; omission accepts either supported algorithm" Enums(v1,md5,hmac-sha256)
|
||||||
|
// @Success 200 {object} dto.Response{data=dto.APIKeyMutationResult} "code=200 on success; data is null on errors"
|
||||||
|
// @Failure 403 {object} dto.Response "Invalid or missing CSRF token for a session request"
|
||||||
|
// @Security ApiKeyAuth || Timestamp
|
||||||
|
// @Security PanelSession || CSRFToken
|
||||||
|
// @Router /core/auth/api/keys/delete [post]
|
||||||
|
func (b *BaseApi) DeleteAPIKey(c *gin.Context) {
|
||||||
|
var req dto.APIKeyMutation
|
||||||
|
if !bindAPIKeyRequest(c, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.NewAPIKeyService().Delete(c, req); err != nil {
|
||||||
|
apiKeyError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apiKeyMutationSuccess(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags API Key
|
||||||
|
// @Summary Reveal an independent API key's plaintext secret
|
||||||
|
// @Description Requires a panel login session and CSRF token. API Key authentication is not supported, including requests that also contain a valid session cookie; omit all 1Panel authentication headers.
|
||||||
|
// @Description Only the current account's independent keys can be revealed. Submit the current revision returned by search. Legacy, revoked and deleted keys are rejected; disabled or expired keys can still be revealed.
|
||||||
|
// @Description Secret disclosure is audited. Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500. The response must not be cached or logged.
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.APIKeyMutation true "Independent key ID and current revision"
|
||||||
|
// @Success 200 {object} dto.Response{data=dto.APIKeyRevealed} "code=200 on success; data is null on errors"
|
||||||
|
// @Failure 403 {object} dto.Response "Invalid or missing CSRF token"
|
||||||
|
// @Header 200 {string} Cache-Control "no-store"
|
||||||
|
// @Security PanelSession || CSRFToken
|
||||||
|
// @Router /core/auth/api/keys/reveal [post]
|
||||||
|
func (b *BaseApi) RevealAPIKey(c *gin.Context) {
|
||||||
|
c.Header("Cache-Control", "no-store")
|
||||||
|
if _, err := auth.RequireAPIKeySession(c); err != nil {
|
||||||
|
helper.BadAuth(c, "ErrNotLogin", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req dto.APIKeyMutation
|
||||||
|
if !bindAPIKeyRequest(c, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := service.NewAPIKeyService().Reveal(c, req)
|
||||||
|
if err != nil {
|
||||||
|
apiKeyError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -417,11 +417,13 @@ func (b *BaseApi) MFAClose(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Summary generate api key
|
// @Summary Generate or reset the legacy API key
|
||||||
|
// @Description Requires a panel login session and CSRF token. API Key authentication is not supported. This replaces the current account's legacy secret; existing clients using the previous secret must be updated. Independent API keys are unaffected.
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Success 200 {string} key
|
// @Produce json
|
||||||
// @Security ApiKeyAuth
|
// @Success 200 {object} dto.Response{data=string} "code=200 with the new secret; authentication and business errors use envelope code 400, 401 or 500"
|
||||||
// @Security Timestamp
|
// @Header 200 {string} Cache-Control "no-store"
|
||||||
|
// @Security PanelSession || CSRFToken
|
||||||
// @Router /core/auth/api/generate [post]
|
// @Router /core/auth/api/generate [post]
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"}
|
||||||
func (b *BaseApi) GenerateApiKey(c *gin.Context) {
|
func (b *BaseApi) GenerateApiKey(c *gin.Context) {
|
||||||
@@ -440,12 +442,13 @@ func (b *BaseApi) GenerateApiKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Summary Update api config
|
// @Summary Update the legacy API key configuration
|
||||||
|
// @Description Requires a panel login session and CSRF token. API Key authentication is not supported. This updates only the current account's legacy API configuration.
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
// @Param request body dto.ApiInterfaceConfig true "request"
|
// @Param request body dto.ApiInterfaceConfig true "request"
|
||||||
// @Success 200
|
// @Success 200 {object} dto.Response "code=200 on success; authentication and business errors use envelope code 400, 401 or 500"
|
||||||
// @Security ApiKeyAuth
|
// @Security PanelSession || CSRFToken
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /core/auth/api/update [post]
|
// @Router /core/auth/api/update [post]
|
||||||
// @x-panel-log {"bodyKeys":["ipWhiteList","apiTrustedProxies"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]","formatEN":"update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"}
|
// @x-panel-log {"bodyKeys":["ipWhiteList","apiTrustedProxies"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]","formatEN":"update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"}
|
||||||
func (b *BaseApi) UpdateApiConfig(c *gin.Context) {
|
func (b *BaseApi) UpdateApiConfig(c *gin.Context) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -42,7 +43,7 @@ func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessH
|
|||||||
ownerProvider = ownerProviders[0]
|
ownerProvider = ownerProviders[0]
|
||||||
}
|
}
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") {
|
if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") && !isAPIKeyManagementRequest(c) {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -97,6 +98,20 @@ func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessH
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAPIKeyManagementRequest(c *gin.Context) bool {
|
||||||
|
if c.Request.Method != http.MethodPost {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch c.Request.URL.Path {
|
||||||
|
case "/api/v2/core/auth/api/keys/search", "/api/v2/core/auth/api/keys/create",
|
||||||
|
"/api/v2/core/auth/api/keys/update", "/api/v2/core/auth/api/keys/status",
|
||||||
|
"/api/v2/core/auth/api/keys/revoke", "/api/v2/core/auth/api/keys/delete":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func LoadAPIAuthConfig(_ *gin.Context) (APIAuthConfig, error) {
|
func LoadAPIAuthConfig(_ *gin.Context) (APIAuthConfig, error) {
|
||||||
settingRepo := repo.NewISettingRepo()
|
settingRepo := repo.NewISettingRepo()
|
||||||
config := APIAuthConfig{}
|
config := APIAuthConfig{}
|
||||||
|
|||||||
@@ -66,6 +66,26 @@ func RequireAPIKeySession(c *gin.Context) (psession.SessionUser, error) {
|
|||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RequireAPIKeyManagement(c *gin.Context) (psession.SessionUser, error) {
|
||||||
|
if c == nil || !c.GetBool("API_AUTH") {
|
||||||
|
return RequireAPIKeySession(c)
|
||||||
|
}
|
||||||
|
if c.GetBool("LOCAL_REQUEST") {
|
||||||
|
return psession.SessionUser{}, buserr.New("ErrNotLogin")
|
||||||
|
}
|
||||||
|
kind, keyID := c.GetString("API_AUTH_KEY_KIND"), c.GetString("API_AUTH_KEY_ID")
|
||||||
|
if (kind != "apiKey" && kind != "legacy") || keyID == "" || (kind == "legacy") != (keyID == "legacy") {
|
||||||
|
return psession.SessionUser{}, buserr.New("ErrNotLogin")
|
||||||
|
}
|
||||||
|
ownerType, ownerID := c.GetString("API_AUTH_OWNER_TYPE"), c.GetString("API_AUTH_OWNER_ID")
|
||||||
|
value, _ := c.Get(psession.GinContextSessionUserKey)
|
||||||
|
u, ok := value.(psession.SessionUser)
|
||||||
|
if (ownerType != APIKeyOwnerPanelAdmin && ownerType != APIKeyOwnerEnterpriseUser) || ownerID == "" || !ok || u.ID != ownerID || u.Name == "" {
|
||||||
|
return psession.SessionUser{}, buserr.New("ErrNotLogin")
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
func HasAPICredentials(c *gin.Context) bool {
|
func HasAPICredentials(c *gin.Context) bool {
|
||||||
return c.GetHeader("1Panel-Token") != "" || c.GetHeader("1Panel-Timestamp") != "" || c.GetHeader("1Panel-Key-ID") != "" || c.GetHeader("1Panel-Signature-Version") != ""
|
return c.GetHeader("1Panel-Token") != "" || c.GetHeader("1Panel-Timestamp") != "" || c.GetHeader("1Panel-Key-ID") != "" || c.GetHeader("1Panel-Signature-Version") != ""
|
||||||
}
|
}
|
||||||
@@ -87,7 +107,7 @@ func LegacyAPIKeyFingerprint(owner APIKeyOwner, config APIAuthConfig, policy mod
|
|||||||
}
|
}
|
||||||
|
|
||||||
func APIKeySecret(key model.APIKey) (string, error) {
|
func APIKeySecret(key model.APIKey) (string, error) {
|
||||||
if key.SecretVersion != 1 || key.SecretCiphertext == "" {
|
if key.DeletedAt != nil || key.Status == "Revoked" || key.SecretVersion != 1 || key.SecretCiphertext == "" {
|
||||||
return "", buserr.New("ErrApiConfigKeyInvalid")
|
return "", buserr.New("ErrApiConfigKeyInvalid")
|
||||||
}
|
}
|
||||||
master, err := encrypt.APIKeyEncryptionKey()
|
master, err := encrypt.APIKeyEncryptionKey()
|
||||||
@@ -98,7 +118,7 @@ func APIKeySecret(key model.APIKey) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ValidateAPIKeyState(key model.APIKey, now time.Time) error {
|
func ValidateAPIKeyState(key model.APIKey, now time.Time) error {
|
||||||
if key.Status != constant.StatusEnable {
|
if key.DeletedAt != nil || key.Status != constant.StatusEnable {
|
||||||
return buserr.New("ErrApiConfigStatusInvalid")
|
return buserr.New("ErrApiConfigStatusInvalid")
|
||||||
}
|
}
|
||||||
if key.ExpiresAt != nil && !now.Before(*key.ExpiresAt) {
|
if key.ExpiresAt != nil && !now.Before(*key.ExpiresAt) {
|
||||||
@@ -113,7 +133,7 @@ func configForAPIKey(key model.APIKey, secret string, owner APIKeyOwner) APIAuth
|
|||||||
|
|
||||||
func loadMultiAPIKeyConfig(c *gin.Context, legacy APIAuthConfigLoader, provider APIKeyOwnerProvider) (APIAuthConfig, error) {
|
func loadMultiAPIKeyConfig(c *gin.Context, legacy APIAuthConfigLoader, provider APIKeyOwnerProvider) (APIAuthConfig, error) {
|
||||||
keyID := c.GetHeader("1Panel-Key-ID")
|
keyID := c.GetHeader("1Panel-Key-ID")
|
||||||
q := global.DB.Model(&model.APIKey{}).Select("id", "secret_ciphertext", "secret_version", "revision")
|
q := global.DB.Model(&model.APIKey{}).Where("deleted_at IS NULL").Select("id", "secret_ciphertext", "secret_version", "revision")
|
||||||
if keyID != "" {
|
if keyID != "" {
|
||||||
q = q.Where("id = ?", keyID)
|
q = q.Where("id = ?", keyID)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+35
-27
@@ -6,41 +6,42 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type APIKeyItem struct {
|
type APIKeyItem struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id" example:"8a8f0f77-1ec8-4d46-8f5a-0ea8807e2d8a"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind" enums:"apiKey,legacy" example:"apiKey"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" example:"Deployment automation"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description" example:"Used by the deployment service"`
|
||||||
KeyHint string `json:"keyHint"`
|
KeyHint string `json:"keyHint" example:"abcd••••1234"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status" enums:"Enable,Disable,Revoked,Expired" example:"Enable"`
|
||||||
IPWhiteList string `json:"ipWhiteList"`
|
IPWhiteList string `json:"ipWhiteList" example:"192.0.2.10"`
|
||||||
APITrustedProxies string `json:"apiTrustedProxies"`
|
APITrustedProxies string `json:"apiTrustedProxies" example:"192.0.2.1"`
|
||||||
APIKeyValidityTime int `json:"apiKeyValidityTime"`
|
APIKeyValidityTime int `json:"apiKeyValidityTime" example:"120"`
|
||||||
ExpiresAt *time.Time `json:"expiresAt"`
|
ExpiresAt *time.Time `json:"expiresAt" format:"date-time" extensions:"x-nullable"`
|
||||||
AllowAppBinding bool `json:"allowAppBinding"`
|
AllowAppBinding bool `json:"allowAppBinding" example:"false"`
|
||||||
Revision uint64 `json:"revision"`
|
Revision uint64 `json:"revision" minimum:"1" example:"1"`
|
||||||
CreatedAt *time.Time `json:"createdAt"`
|
CreatedAt *time.Time `json:"createdAt" format:"date-time" extensions:"x-nullable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIKeySearch struct {
|
type APIKeySearch struct {
|
||||||
Page int `json:"page"`
|
Page int `json:"page" minimum:"1" example:"1"`
|
||||||
PageSize int `json:"pageSize"`
|
PageSize int `json:"pageSize" minimum:"1" maximum:"100" example:"20"`
|
||||||
ExcludeRevoked bool `json:"excludeRevoked"`
|
ExcludeRevoked bool `json:"excludeRevoked" default:"false"`
|
||||||
}
|
}
|
||||||
type APIKeyPage struct {
|
type APIKeyPage struct {
|
||||||
Items []APIKeyItem `json:"items"`
|
Items []APIKeyItem `json:"items"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
}
|
}
|
||||||
type APIKeyFields struct {
|
type APIKeyFields struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name" minLength:"1" maxLength:"64" example:"Deployment automation"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description" maxLength:"256" example:"Used by the deployment service"`
|
||||||
IPWhiteList string `json:"ipWhiteList"`
|
IPWhiteList string `json:"ipWhiteList" maxLength:"4096" example:"192.0.2.10"`
|
||||||
APITrustedProxies string `json:"apiTrustedProxies"`
|
APITrustedProxies string `json:"apiTrustedProxies" maxLength:"4096" example:"192.0.2.1"`
|
||||||
APIKeyValidityTime int `json:"apiKeyValidityTime"`
|
APIKeyValidityTime int `json:"apiKeyValidityTime" minimum:"0" maximum:"1440" example:"120"`
|
||||||
ExpiresAt json.RawMessage `json:"expiresAt"`
|
ExpiresAt json.RawMessage `json:"expiresAt" swaggertype:"string" format:"date-time" extensions:"x-nullable" example:"2030-01-01T00:00:00Z"`
|
||||||
AllowAppBinding bool `json:"allowAppBinding"`
|
AllowAppBinding bool `json:"allowAppBinding" default:"false"`
|
||||||
}
|
}
|
||||||
type APIKeyCreate struct {
|
type APIKeyCreate struct {
|
||||||
RequestID string `json:"requestID"`
|
// Optional client-generated UUID for idempotent retries. Omitted or empty values are generated by the server.
|
||||||
|
RequestID string `json:"requestID" format:"uuid" example:"df1a75a3-5f56-4fa3-9e7a-a573a853d894"`
|
||||||
APIKeyFields
|
APIKeyFields
|
||||||
}
|
}
|
||||||
type APIKeyCreated struct {
|
type APIKeyCreated struct {
|
||||||
@@ -49,8 +50,15 @@ type APIKeyCreated struct {
|
|||||||
AlreadyCreated bool `json:"alreadyCreated,omitempty"`
|
AlreadyCreated bool `json:"alreadyCreated,omitempty"`
|
||||||
}
|
}
|
||||||
type APIKeyMutation struct {
|
type APIKeyMutation struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id" example:"8a8f0f77-1ec8-4d46-8f5a-0ea8807e2d8a"`
|
||||||
Revision uint64 `json:"revision"`
|
Revision uint64 `json:"revision" minimum:"1" example:"1"`
|
||||||
|
}
|
||||||
|
type APIKeyMutationResult struct {
|
||||||
|
TerminalClosePending bool `json:"terminalClosePending"`
|
||||||
|
}
|
||||||
|
type APIKeyRevealed struct {
|
||||||
|
APIKeyMutation
|
||||||
|
APIKey string `json:"apiKey"`
|
||||||
}
|
}
|
||||||
type APIKeyUpdate struct {
|
type APIKeyUpdate struct {
|
||||||
APIKeyMutation
|
APIKeyMutation
|
||||||
@@ -58,5 +66,5 @@ type APIKeyUpdate struct {
|
|||||||
}
|
}
|
||||||
type APIKeyStatus struct {
|
type APIKeyStatus struct {
|
||||||
APIKeyMutation
|
APIKeyMutation
|
||||||
Status string `json:"status"`
|
Status string `json:"status" enums:"Enable,Disable" example:"Disable"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type APIKey struct {
|
|||||||
CreatedAt time.Time `json:"-"`
|
CreatedAt time.Time `json:"-"`
|
||||||
UpdatedAt time.Time `json:"-"`
|
UpdatedAt time.Time `json:"-"`
|
||||||
RevokedAt *time.Time `json:"-"`
|
RevokedAt *time.Time `json:"-"`
|
||||||
|
DeletedAt *time.Time `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LegacyAPIKeyPolicy struct {
|
type LegacyAPIKeyPolicy struct {
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ func APIKeyOwnerQuery(db *gorm.DB, ownerType, ownerID string) *gorm.DB {
|
|||||||
}
|
}
|
||||||
func (r *APIKeyRepo) Get(id string) (model.APIKey, error) {
|
func (r *APIKeyRepo) Get(id string) (model.APIKey, error) {
|
||||||
var key model.APIKey
|
var key model.APIKey
|
||||||
err := global.DB.Where("id = ?", id).First(&key).Error
|
err := global.DB.Where("id = ? AND deleted_at IS NULL", id).First(&key).Error
|
||||||
return key, err
|
return key, err
|
||||||
}
|
}
|
||||||
func (r *APIKeyRepo) List(ownerType, ownerID string, excludeRevoked bool) ([]model.APIKey, error) {
|
func (r *APIKeyRepo) List(ownerType, ownerID string, excludeRevoked bool) ([]model.APIKey, error) {
|
||||||
items := make([]model.APIKey, 0)
|
items := make([]model.APIKey, 0)
|
||||||
q := APIKeyOwnerQuery(global.DB, ownerType, ownerID)
|
q := APIKeyOwnerQuery(global.DB, ownerType, ownerID).Where("deleted_at IS NULL")
|
||||||
if excludeRevoked {
|
if excludeRevoked {
|
||||||
q = q.Where("status <> ?", "Revoked")
|
q = q.Where("status <> ?", "Revoked")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,27 @@ func (s *APIKeyService) owner(c *gin.Context) (auth.APIKeyOwner, error) {
|
|||||||
return s.Provider.CurrentAPIKeyOwner(c)
|
return s.Provider.CurrentAPIKeyOwner(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *APIKeyService) managementOwner(c *gin.Context) (auth.APIKeyOwner, error) {
|
||||||
|
if _, err := auth.RequireAPIKeyManagement(c); err != nil {
|
||||||
|
return auth.APIKeyOwner{}, err
|
||||||
|
}
|
||||||
|
if s.Provider == nil {
|
||||||
|
return auth.APIKeyOwner{}, buserr.New("ErrApiConfigStatusInvalid")
|
||||||
|
}
|
||||||
|
if !c.GetBool("API_AUTH") {
|
||||||
|
return s.Provider.CurrentAPIKeyOwner(c)
|
||||||
|
}
|
||||||
|
ownerType, ownerID := c.GetString("API_AUTH_OWNER_TYPE"), c.GetString("API_AUTH_OWNER_ID")
|
||||||
|
owner, err := s.Provider.ResolveAPIKeyOwner(c, ownerType, ownerID)
|
||||||
|
if err != nil {
|
||||||
|
return auth.APIKeyOwner{}, err
|
||||||
|
}
|
||||||
|
if owner.Type != ownerType || owner.ID != ownerID {
|
||||||
|
return auth.APIKeyOwner{}, buserr.New("ErrNotLogin")
|
||||||
|
}
|
||||||
|
return owner, nil
|
||||||
|
}
|
||||||
|
|
||||||
func apiKeyItem(key model.APIKey) dto.APIKeyItem {
|
func apiKeyItem(key model.APIKey) dto.APIKeyItem {
|
||||||
status := key.Status
|
status := key.Status
|
||||||
if status != "Revoked" && key.ExpiresAt != nil && !time.Now().Before(*key.ExpiresAt) {
|
if status != "Revoked" && key.ExpiresAt != nil && !time.Now().Before(*key.ExpiresAt) {
|
||||||
@@ -75,7 +96,7 @@ func (s *APIKeyService) legacy(owner auth.APIKeyOwner) (dto.APIKeyItem, auth.API
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIKeyService) Search(c *gin.Context, req dto.APIKeySearch) (*dto.APIKeyPage, error) {
|
func (s *APIKeyService) Search(c *gin.Context, req dto.APIKeySearch) (*dto.APIKeyPage, error) {
|
||||||
owner, err := s.owner(c)
|
owner, err := s.managementOwner(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -146,11 +167,13 @@ func keyHint(secret string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIKeyService) Create(c *gin.Context, req dto.APIKeyCreate) (*dto.APIKeyCreated, error) {
|
func (s *APIKeyService) Create(c *gin.Context, req dto.APIKeyCreate) (*dto.APIKeyCreated, error) {
|
||||||
owner, err := s.owner(c)
|
owner, err := s.managementOwner(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err = uuid.Parse(req.RequestID); err != nil {
|
if req.RequestID == "" {
|
||||||
|
req.RequestID = uuid.NewString()
|
||||||
|
} else if _, err = uuid.Parse(req.RequestID); err != nil {
|
||||||
return nil, buserr.New("ErrInvalidParams")
|
return nil, buserr.New("ErrInvalidParams")
|
||||||
}
|
}
|
||||||
expires, err := validateAPIKeyFields(&req.APIKeyFields, true)
|
expires, err := validateAPIKeyFields(&req.APIKeyFields, true)
|
||||||
@@ -172,6 +195,9 @@ func (s *APIKeyService) Create(c *gin.Context, req dto.APIKeyCreate) (*dto.APIKe
|
|||||||
q := repo.APIKeyOwnerQuery(tx, owner.Type, owner.ID)
|
q := repo.APIKeyOwnerQuery(tx, owner.Type, owner.ID)
|
||||||
err := q.Where("request_id = ?", req.RequestID).First(&key).Error
|
err := q.Where("request_id = ?", req.RequestID).First(&key).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if key.DeletedAt != nil {
|
||||||
|
return buserr.New("ErrAPIKeyNotFound")
|
||||||
|
}
|
||||||
if key.RequestHash != requestHash {
|
if key.RequestHash != requestHash {
|
||||||
return buserr.New("ErrAPIKeyConflict")
|
return buserr.New("ErrAPIKeyConflict")
|
||||||
}
|
}
|
||||||
@@ -219,7 +245,7 @@ func ensureAPIKeyName(tx *gorm.DB, owner auth.APIKeyOwner, name, id string) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIKeyService) Update(c *gin.Context, req dto.APIKeyUpdate) error {
|
func (s *APIKeyService) Update(c *gin.Context, req dto.APIKeyUpdate) error {
|
||||||
owner, err := s.owner(c)
|
owner, err := s.managementOwner(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -247,7 +273,7 @@ func (s *APIKeyService) Update(c *gin.Context, req dto.APIKeyUpdate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIKeyService) Status(c *gin.Context, req dto.APIKeyStatus) error {
|
func (s *APIKeyService) Status(c *gin.Context, req dto.APIKeyStatus) error {
|
||||||
owner, err := s.owner(c)
|
owner, err := s.managementOwner(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -271,7 +297,7 @@ func (s *APIKeyService) Status(c *gin.Context, req dto.APIKeyStatus) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIKeyService) Revoke(c *gin.Context, req dto.APIKeyMutation) error {
|
func (s *APIKeyService) Revoke(c *gin.Context, req dto.APIKeyMutation) error {
|
||||||
owner, err := s.owner(c)
|
owner, err := s.managementOwner(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -289,21 +315,75 @@ func (s *APIKeyService) Revoke(c *gin.Context, req dto.APIKeyMutation) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *APIKeyService) Delete(c *gin.Context, req dto.APIKeyMutation) error {
|
||||||
|
owner, err := s.managementOwner(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.ID == "legacy" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
|
return s.mutate(c, owner, req, "delete", func(_ *gorm.DB, key *model.APIKey) error {
|
||||||
|
now := time.Now()
|
||||||
|
key.Status = "Revoked"
|
||||||
|
if key.RevokedAt == nil {
|
||||||
|
key.RevokedAt = &now
|
||||||
|
}
|
||||||
|
key.DeletedAt = &now
|
||||||
|
key.ActiveName = nil
|
||||||
|
key.SecretCiphertext = ""
|
||||||
|
key.KeyHint = ""
|
||||||
|
key.AllowAppBinding = false
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APIKeyService) Reveal(c *gin.Context, req dto.APIKeyMutation) (*dto.APIKeyRevealed, error) {
|
||||||
|
owner, err := s.owner(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.ID == "" || req.ID == "legacy" || req.Revision == 0 {
|
||||||
|
return nil, buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
|
// Serialize the snapshot and disclosure audit with revocation/deletion.
|
||||||
|
apiKeyMutationMu.Lock()
|
||||||
|
defer apiKeyMutationMu.Unlock()
|
||||||
|
var key model.APIKey
|
||||||
|
if err = repo.APIKeyOwnerQuery(global.DB, owner.Type, owner.ID).Where("id = ? AND deleted_at IS NULL", req.ID).First(&key).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, buserr.New("ErrAPIKeyNotFound")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if key.Revision != req.Revision || key.Status == "Revoked" {
|
||||||
|
return nil, buserr.New("ErrAPIKeyConflict")
|
||||||
|
}
|
||||||
|
secret, err := auth.APIKeySecret(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, buserr.New("ErrApiConfigKeyInvalid")
|
||||||
|
}
|
||||||
|
if err = apikey_audit.Record(c, apikey_audit.Event{OwnerType: owner.Type, OwnerID: owner.ID, KeyID: key.ID, KeyName: key.Name, Action: "reveal", Status: constant.StatusSuccess}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dto.APIKeyRevealed{APIKeyMutation: req, APIKey: secret}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *APIKeyService) mutate(c *gin.Context, owner auth.APIKeyOwner, req dto.APIKeyMutation, action string, change func(*gorm.DB, *model.APIKey) error) error {
|
func (s *APIKeyService) mutate(c *gin.Context, owner auth.APIKeyOwner, req dto.APIKeyMutation, action string, change func(*gorm.DB, *model.APIKey) error) error {
|
||||||
if req.ID == "" || req.Revision == 0 {
|
if req.ID == "" || req.Revision == 0 {
|
||||||
return buserr.New("ErrInvalidParams")
|
return buserr.New("ErrInvalidParams")
|
||||||
}
|
}
|
||||||
apiKeyMutationMu.Lock()
|
apiKeyMutationMu.Lock()
|
||||||
var key model.APIKey
|
var key model.APIKey
|
||||||
closeTerminals := action == "disable" || action == "revoke"
|
closeTerminals := action == "disable" || action == "revoke" || action == "delete"
|
||||||
err := global.DB.Transaction(func(tx *gorm.DB) error {
|
err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := repo.APIKeyOwnerQuery(tx, owner.Type, owner.ID).Where("id = ?", req.ID).First(&key).Error; err != nil {
|
if err := repo.APIKeyOwnerQuery(tx, owner.Type, owner.ID).Where("id = ? AND deleted_at IS NULL", req.ID).First(&key).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return buserr.New("ErrAPIKeyNotFound")
|
return buserr.New("ErrAPIKeyNotFound")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if key.Revision != req.Revision || key.Status == "Revoked" {
|
if key.Revision != req.Revision || (key.Status == "Revoked" && action != "delete") {
|
||||||
return buserr.New("ErrAPIKeyConflict")
|
return buserr.New("ErrAPIKeyConflict")
|
||||||
}
|
}
|
||||||
before := key
|
before := key
|
||||||
|
|||||||
+2140
-264
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,10 @@ import (
|
|||||||
// @schemes http https
|
// @schemes http https
|
||||||
|
|
||||||
// @securityDefinitions.apikey ApiKeyAuth
|
// @securityDefinitions.apikey ApiKeyAuth
|
||||||
// @description Custom Token Format, Format: md5('1panel' + API-Key + UnixTimestamp).
|
// @description Signed API Key authentication. Send the signature, never the plaintext API Key, in 1Panel-Token.
|
||||||
|
// @description Supported formats: hex(HMAC-SHA256(API-Key, '1panel:' + UnixTimestamp)) or md5('1panel' + API-Key + UnixTimestamp).
|
||||||
|
// @description Send the matching Unix timestamp in 1Panel-Timestamp. Optionally set 1Panel-Signature-Version to hmac-sha256, v1 or md5; omission accepts either algorithm.
|
||||||
|
// @description Independent keys may include their ID in 1Panel-Key-ID to select the credential; omit that header for a legacy key.
|
||||||
// @description ```
|
// @description ```
|
||||||
// @description eg:
|
// @description eg:
|
||||||
// @description curl -X GET "http://{host}:{port}/api/v2/toolbox/device/base" \
|
// @description curl -X GET "http://{host}:{port}/api/v2/toolbox/device/base" \
|
||||||
@@ -27,7 +30,7 @@ import (
|
|||||||
// @description ```
|
// @description ```
|
||||||
// @description - `1Panel-Token` is the key for the panel API Key.
|
// @description - `1Panel-Token` is the key for the panel API Key.
|
||||||
// @type apiKey
|
// @type apiKey
|
||||||
// @in Header
|
// @in header
|
||||||
// @name 1Panel-Token
|
// @name 1Panel-Token
|
||||||
// @securityDefinitions.apikey Timestamp
|
// @securityDefinitions.apikey Timestamp
|
||||||
// @type apiKey
|
// @type apiKey
|
||||||
|
|||||||
@@ -72,5 +72,6 @@ func coreMigrations() []*gormigrate.Migration {
|
|||||||
migrations.UpdateAiMenuOrder,
|
migrations.UpdateAiMenuOrder,
|
||||||
migrations.UpdateAiMenuOrderV2,
|
migrations.UpdateAiMenuOrderV2,
|
||||||
migrations.AddPanelMCP,
|
migrations.AddPanelMCP,
|
||||||
|
migrations.AddAPIKeyDeletedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,3 +15,8 @@ var AddOperationLogAPIKey = &gormigrate.Migration{
|
|||||||
ID: "20260915-add-operation-log-api-key",
|
ID: "20260915-add-operation-log-api-key",
|
||||||
Migrate: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.OperationLog{}) },
|
Migrate: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.OperationLog{}) },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AddAPIKeyDeletedAt = &gormigrate.Migration{
|
||||||
|
ID: "20261028-api-key-deleted-at",
|
||||||
|
Migrate: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.APIKey{}) },
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ func OperationLog() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch c.Request.URL.Path {
|
switch c.Request.URL.Path {
|
||||||
|
case "/api/v2/core/auth/api/keys/reveal":
|
||||||
|
// Disclosure has its own audit and must not enter response-body capture.
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
case "/api/v2/core/mcp/credentials/create", "/api/v2/core/mcp/credentials/reveal", "/api/v2/core/mcp/credentials/rotate", "/api/v2/core/mcp/credentials/export":
|
case "/api/v2/core/mcp/credentials/create", "/api/v2/core/mcp/credentials/reveal", "/api/v2/core/mcp/credentials/rotate", "/api/v2/core/mcp/credentials/export":
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ func (s *BaseRouter) InitRouter(Router *gin.RouterGroup) {
|
|||||||
authRouter.POST("/api/keys/update", baseApi.UpdateAPIKey)
|
authRouter.POST("/api/keys/update", baseApi.UpdateAPIKey)
|
||||||
authRouter.POST("/api/keys/status", baseApi.SetAPIKeyStatus)
|
authRouter.POST("/api/keys/status", baseApi.SetAPIKeyStatus)
|
||||||
authRouter.POST("/api/keys/revoke", baseApi.RevokeAPIKey)
|
authRouter.POST("/api/keys/revoke", baseApi.RevokeAPIKey)
|
||||||
|
authRouter.POST("/api/keys/delete", baseApi.DeleteAPIKey)
|
||||||
|
authRouter.POST("/api/keys/reveal", baseApi.RevealAPIKey)
|
||||||
|
|
||||||
authRouter.GET("/current", baseApi.GetCurrentUser)
|
authRouter.GET("/current", baseApi.GetCurrentUser)
|
||||||
authRouter.POST("/current/update", baseApi.UpdateCurrentUser)
|
authRouter.POST("/current/update", baseApi.UpdateCurrentUser)
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ func Build(c *gin.Context, event Event) (*model.OperationLog, error) {
|
|||||||
"enable": {"启用 API Key", "Enable API key"},
|
"enable": {"启用 API Key", "Enable API key"},
|
||||||
"disable": {"停用 API Key", "Disable API key"},
|
"disable": {"停用 API Key", "Disable API key"},
|
||||||
"revoke": {"撤销 API Key", "Revoke API key"},
|
"revoke": {"撤销 API Key", "Revoke API key"},
|
||||||
|
"delete": {"删除 API Key", "Delete API key"},
|
||||||
|
"reveal": {"查看 API Key", "Reveal API key"},
|
||||||
"app_binding": {"向 APP 交付 API Key", "Deliver API key to APP"},
|
"app_binding": {"向 APP 交付 API Key", "Deliver API key to APP"},
|
||||||
}
|
}
|
||||||
label, ok := labels[event.Action]
|
label, ok := labels[event.Action]
|
||||||
@@ -80,6 +82,17 @@ func Build(c *gin.Context, event Event) (*model.OperationLog, error) {
|
|||||||
record.UserAgent = c.Request.UserAgent()
|
record.UserAgent = c.Request.UserAgent()
|
||||||
record.IP = c.ClientIP()
|
record.IP = c.ClientIP()
|
||||||
}
|
}
|
||||||
|
if c.GetBool("API_AUTH") && event.Action != "app_binding" {
|
||||||
|
record.AuthMethod = "api_key"
|
||||||
|
record.APIKeyID = c.GetString("API_AUTH_KEY_ID")
|
||||||
|
record.APIKeyName = c.GetString("API_AUTH_KEY_NAME")
|
||||||
|
if c.GetString("API_AUTH_KEY_KIND") == "legacy" {
|
||||||
|
record.APIKeyID = "legacy:" + c.GetString("API_AUTH_OWNER_TYPE") + ":" + c.GetString("API_AUTH_OWNER_ID")
|
||||||
|
}
|
||||||
|
if clientIP := c.GetString("API_AUTH_CLIENT_IP"); clientIP != "" {
|
||||||
|
record.IP = clientIP
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if event.ClientIP != "" {
|
if event.ClientIP != "" {
|
||||||
record.IP = event.ClientIP
|
record.IP = event.ClientIP
|
||||||
|
|||||||
@@ -30,4 +30,7 @@ export namespace APIKey {
|
|||||||
id: string;
|
id: string;
|
||||||
revision: number;
|
revision: number;
|
||||||
}
|
}
|
||||||
|
export interface Revealed extends Reference {
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const prefix = '/core/auth/api/keys';
|
|||||||
export const searchAPIKeys = (params: { page: number; pageSize: number; excludeRevoked?: boolean }) => {
|
export const searchAPIKeys = (params: { page: number; pageSize: number; excludeRevoked?: boolean }) => {
|
||||||
return http.post<APIKey.Search>(`${prefix}/search`, params);
|
return http.post<APIKey.Search>(`${prefix}/search`, params);
|
||||||
};
|
};
|
||||||
export const createAPIKey = (params: APIKey.Editable & { requestID: string }) => {
|
export const createAPIKey = (params: APIKey.Editable & { requestID?: string }) => {
|
||||||
return http.post<APIKey.Created>(`${prefix}/create`, params);
|
return http.post<APIKey.Created>(`${prefix}/create`, params);
|
||||||
};
|
};
|
||||||
export const updateAPIKey = (params: APIKey.Editable & APIKey.Reference) => {
|
export const updateAPIKey = (params: APIKey.Editable & APIKey.Reference) => {
|
||||||
@@ -18,3 +18,9 @@ export const setAPIKeyStatus = (params: APIKey.Reference & { status: 'Enable' |
|
|||||||
export const revokeAPIKey = (params: APIKey.Reference) => {
|
export const revokeAPIKey = (params: APIKey.Reference) => {
|
||||||
return http.post<{ terminalClosePending: boolean }>(`${prefix}/revoke`, params);
|
return http.post<{ terminalClosePending: boolean }>(`${prefix}/revoke`, params);
|
||||||
};
|
};
|
||||||
|
export const deleteAPIKey = (params: APIKey.Reference) => {
|
||||||
|
return http.post<{ terminalClosePending: boolean }>(`${prefix}/delete`, params);
|
||||||
|
};
|
||||||
|
export const revealAPIKey = (params: APIKey.Reference) => {
|
||||||
|
return http.post<APIKey.Revealed>(`${prefix}/reveal`, params);
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,27 +6,14 @@
|
|||||||
:confirm-before-close="true"
|
:confirm-before-close="true"
|
||||||
@before-close="beforeClose"
|
@before-close="beforeClose"
|
||||||
>
|
>
|
||||||
<template v-if="created">
|
<el-form
|
||||||
<el-alert
|
ref="formRef"
|
||||||
:title="$t(secret ? 'apiKeyManagement.saveSecret' : 'apiKeyManagement.alreadyCreated')"
|
:model="form"
|
||||||
:type="secret ? 'warning' : 'info'"
|
:rules="rules"
|
||||||
:closable="false"
|
label-position="top"
|
||||||
show-icon
|
class="text-[var(--el-text-color-regular)]"
|
||||||
class="mb-4"
|
@submit.prevent="submit"
|
||||||
/>
|
>
|
||||||
<el-form label-position="top">
|
|
||||||
<el-form-item :label="$t('commons.table.name')">{{ created.name }}</el-form-item>
|
|
||||||
<el-form-item v-if="secret" :label="$t('setting.apiKey')">
|
|
||||||
<el-input :model-value="secret" readonly autocomplete="off">
|
|
||||||
<template #append>
|
|
||||||
<CopyButton :content="secret" :is-icon="false" />
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<el-checkbox v-if="secret" v-model="saved">{{ $t('apiKeyManagement.saved') }}</el-checkbox>
|
|
||||||
</template>
|
|
||||||
<el-form v-else ref="formRef" :model="form" :rules="rules" label-position="top" @submit.prevent="submit">
|
|
||||||
<el-alert v-if="!item" type="warning" :closable="false" class="!mb-4">
|
<el-alert v-if="!item" type="warning" :closable="false" class="!mb-4">
|
||||||
<ul class="m-0 list-disc space-y-1 pl-4 break-words leading-6">
|
<ul class="m-0 list-disc space-y-1 pl-4 break-words leading-6">
|
||||||
<li>
|
<li>
|
||||||
@@ -139,13 +126,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button v-if="!created" :disabled="busy" @click="beforeClose(() => (visible = false))">
|
<el-button :disabled="busy" @click="beforeClose(() => (visible = false))">
|
||||||
{{ $t('commons.button.cancel') }}
|
{{ $t('commons.button.cancel') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="created" type="primary" :disabled="!!secret && !saved" @click="finish">
|
<el-button type="primary" :loading="busy" @click="submit">
|
||||||
{{ $t('commons.button.confirm') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button v-else type="primary" :loading="busy" @click="submit">
|
|
||||||
{{ $t(item ? 'commons.button.save' : 'apiKeyManagement.create') }}
|
{{ $t(item ? 'commons.button.save' : 'apiKeyManagement.create') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -154,7 +138,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
||||||
import { ElMessageBox, type FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
import { isAxiosError } from 'axios';
|
import { isAxiosError } from 'axios';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { onBeforeRouteLeave } from 'vue-router';
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
@@ -174,9 +158,6 @@ const visible = ref(false);
|
|||||||
const busy = ref(false);
|
const busy = ref(false);
|
||||||
const uncertain = ref(false);
|
const uncertain = ref(false);
|
||||||
const item = ref<APIKey.Item>();
|
const item = ref<APIKey.Item>();
|
||||||
const created = ref<APIKey.Item>();
|
|
||||||
const secret = ref('');
|
|
||||||
const saved = ref(false);
|
|
||||||
const formRef = ref<FormInstance>();
|
const formRef = ref<FormInstance>();
|
||||||
const ipMode = ref('restricted');
|
const ipMode = ref('restricted');
|
||||||
const neverExpires = ref(false);
|
const neverExpires = ref(false);
|
||||||
@@ -196,8 +177,6 @@ let editorVersion = 0;
|
|||||||
|
|
||||||
const discardEditor = () => {
|
const discardEditor = () => {
|
||||||
editorVersion++;
|
editorVersion++;
|
||||||
secret.value = '';
|
|
||||||
created.value = undefined;
|
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
busy.value = false;
|
busy.value = false;
|
||||||
};
|
};
|
||||||
@@ -240,9 +219,6 @@ const rules = {
|
|||||||
const open = (existing?: APIKey.Item, fromApp = false) => {
|
const open = (existing?: APIKey.Item, fromApp = false) => {
|
||||||
editorVersion++;
|
editorVersion++;
|
||||||
item.value = existing;
|
item.value = existing;
|
||||||
created.value = undefined;
|
|
||||||
secret.value = '';
|
|
||||||
saved.value = false;
|
|
||||||
uncertain.value = false;
|
uncertain.value = false;
|
||||||
pendingCreate = undefined;
|
pendingCreate = undefined;
|
||||||
requestID = uuidv4();
|
requestID = uuidv4();
|
||||||
@@ -269,19 +245,9 @@ const open = (existing?: APIKey.Item, fromApp = false) => {
|
|||||||
nextTick(() => formRef.value?.clearValidate());
|
nextTick(() => formRef.value?.clearValidate());
|
||||||
};
|
};
|
||||||
|
|
||||||
const finish = () => {
|
const beforeClose = (done: () => void) => {
|
||||||
secret.value = '';
|
|
||||||
visible.value = false;
|
|
||||||
if (created.value) emit('created', created.value);
|
|
||||||
};
|
|
||||||
const beforeClose = async (done: () => void) => {
|
|
||||||
if (busy.value) return;
|
if (busy.value) return;
|
||||||
if (secret.value && !saved.value) {
|
if (uncertain.value) emit('changed');
|
||||||
await ElMessageBox.alert(i18n.global.t('apiKeyManagement.saveSecret'), i18n.global.t('apiKeyManagement.title'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (created.value) finish();
|
|
||||||
else if (uncertain.value) emit('changed');
|
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,9 +276,13 @@ const submit = async () => {
|
|||||||
pendingCreate ??= { ...payload, requestID };
|
pendingCreate ??= { ...payload, requestID };
|
||||||
const response = await createAPIKey(pendingCreate);
|
const response = await createAPIKey(pendingCreate);
|
||||||
if (ticket !== editorVersion) return;
|
if (ticket !== editorVersion) return;
|
||||||
created.value = response.data.item;
|
|
||||||
secret.value = response.data.apiKey;
|
|
||||||
uncertain.value = false;
|
uncertain.value = false;
|
||||||
|
pendingCreate = undefined;
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||||
|
visible.value = false;
|
||||||
|
emit('changed');
|
||||||
|
emit('created', response.data.item);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
emit('changed');
|
emit('changed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -341,16 +311,12 @@ watch(
|
|||||||
{ flush: 'sync' },
|
{ flush: 'sync' },
|
||||||
);
|
);
|
||||||
onBeforeUnmount(discardEditor);
|
onBeforeUnmount(discardEditor);
|
||||||
onBeforeRouteLeave(async (to) => {
|
onBeforeRouteLeave((to) => {
|
||||||
if (!globalStore.isLogin || ['entrance', 'Expired', 'EnterpriseLicenseRequired'].includes(String(to.name))) {
|
if (!globalStore.isLogin || ['entrance', 'Expired', 'EnterpriseLicenseRequired'].includes(String(to.name))) {
|
||||||
discardEditor();
|
discardEditor();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (busy.value) return false;
|
if (busy.value) return false;
|
||||||
if (secret.value && !saved.value) {
|
|
||||||
await ElMessageBox.alert(i18n.global.t('apiKeyManagement.saveSecret'), i18n.global.t('apiKeyManagement.title'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
discardEditor();
|
discardEditor();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: 'Show secret',
|
||||||
|
hideSecret: 'Hide secret',
|
||||||
|
revealHelp: 'Keep this secret secure. Disabled or expired keys remain unusable even when displayed.',
|
||||||
|
deleteConfirm:
|
||||||
|
'Delete “{0}”? Its secret will be destroyed and the key removed from the list. New requests will be rejected and terminal connections closed. Audit records are retained; running jobs are not cancelled. This cannot be undone.',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'Manage API Keys',
|
manage: 'Manage API Keys',
|
||||||
personalEntry: 'Manage your keys, access conditions, and APP binding in Settings.',
|
personalEntry: 'Manage your keys, access conditions, and APP binding in Settings.',
|
||||||
@@ -23,10 +28,6 @@ const message = {
|
|||||||
allowAppBinding: 'Allow APP QR binding',
|
allowAppBinding: 'Allow APP QR binding',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'Disabling QR binding prevents new device bindings. Devices already bound can continue to use the key until it is disabled, revoked, or expires.',
|
'Disabling QR binding prevents new device bindings. Devices already bound can continue to use the key until it is disabled, revoked, or expires.',
|
||||||
saveSecret: 'Copy and save this secret now. After closing, ordinary key management cannot display it again.',
|
|
||||||
saved: 'I have saved the secret',
|
|
||||||
alreadyCreated:
|
|
||||||
'This key was already created. Its secret will not be shown again. If the original response was lost, revoke and recreate it, or use QR binding if allowed.',
|
|
||||||
uncertain:
|
uncertain:
|
||||||
'The request result is uncertain. Retry with the same details to avoid duplicate keys, or close and check the list before creating another.',
|
'The request result is uncertain. Retry with the same details to avoid duplicate keys, or close and check the list before creating another.',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: 'Mostrar secreto',
|
||||||
|
hideSecret: 'Ocultar secreto',
|
||||||
|
revealHelp: 'Guarde el secreto de forma segura. Mostrarlo no reactiva claves deshabilitadas o caducadas.',
|
||||||
|
deleteConfirm:
|
||||||
|
'¿Eliminar «{0}»? Se destruirá el secreto y se quitará la clave de la lista. Se rechazarán nuevas solicitudes y se cerrarán sus terminales. Se conservará la auditoría; las tareas en ejecución no se cancelarán. Esta acción es irreversible.',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'Gestionar claves API',
|
manage: 'Gestionar claves API',
|
||||||
personalEntry: 'Gestiona tus claves, condiciones de acceso y vinculación de la APP en Ajustes.',
|
personalEntry: 'Gestiona tus claves, condiciones de acceso y vinculación de la APP en Ajustes.',
|
||||||
@@ -23,10 +28,6 @@ const message = {
|
|||||||
allowAppBinding: 'Permitir vinculación de APP por QR',
|
allowAppBinding: 'Permitir vinculación de APP por QR',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'Desactivar la vinculación por QR impide nuevas vinculaciones. Los dispositivos ya vinculados pueden seguir usando la clave hasta que se desactive, se revoque o caduque.',
|
'Desactivar la vinculación por QR impide nuevas vinculaciones. Los dispositivos ya vinculados pueden seguir usando la clave hasta que se desactive, se revoque o caduque.',
|
||||||
saveSecret: 'Copia y guarda la clave ahora. Tras cerrar, la gestión normal no podrá mostrarla de nuevo.',
|
|
||||||
saved: 'He guardado la clave',
|
|
||||||
alreadyCreated:
|
|
||||||
'La clave ya se creó y no se mostrará de nuevo. Si se perdió la respuesta original, revócala y crea otra, o usa QR si está permitido.',
|
|
||||||
uncertain:
|
uncertain:
|
||||||
'El resultado es incierto. Reintenta con los mismos datos para evitar duplicados, o cierra y revisa la lista antes de crear otra.',
|
'El resultado es incierto. Reintenta con los mismos datos para evitar duplicados, o cierra y revisa la lista antes de crear otra.',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: 'نمایش کلید محرمانه',
|
||||||
|
hideSecret: 'پنهان کردن کلید محرمانه',
|
||||||
|
revealHelp: 'کلید را در جای امن نگه دارید. نمایش آن، کلید غیرفعال یا منقضیشده را قابل استفاده نمیکند.',
|
||||||
|
deleteConfirm:
|
||||||
|
'«{0}» حذف شود؟ کلید محرمانه نابود و از فهرست حذف میشود. درخواستهای جدید رد و اتصالهای ترمینال مرتبط بسته میشوند. سوابق حسابرسی حفظ و کارهای در حال اجرا لغو نمیشوند. این عملیات برگشتپذیر نیست.',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'مدیریت کلیدهای API',
|
manage: 'مدیریت کلیدهای API',
|
||||||
personalEntry: 'کلیدها، شرایط دسترسی و اتصال برنامه را در تنظیمات مدیریت کنید.',
|
personalEntry: 'کلیدها، شرایط دسترسی و اتصال برنامه را در تنظیمات مدیریت کنید.',
|
||||||
@@ -23,10 +28,6 @@ const message = {
|
|||||||
allowAppBinding: 'اجازه اتصال برنامه با QR',
|
allowAppBinding: 'اجازه اتصال برنامه با QR',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'با غیرفعال کردن اتصال از طریق QR، اتصال دستگاههای جدید متوقف میشود. دستگاههای متصل میتوانند تا زمان غیرفعالسازی، لغو یا انقضای کلید به استفاده از آن ادامه دهند.',
|
'با غیرفعال کردن اتصال از طریق QR، اتصال دستگاههای جدید متوقف میشود. دستگاههای متصل میتوانند تا زمان غیرفعالسازی، لغو یا انقضای کلید به استفاده از آن ادامه دهند.',
|
||||||
saveSecret: 'اکنون کلید را کپی و ذخیره کنید. پس از بستن، مدیریت عادی نمیتواند کلید کامل را دوباره نشان دهد.',
|
|
||||||
saved: 'کلید را ذخیره کردم',
|
|
||||||
alreadyCreated:
|
|
||||||
'این کلید قبلاً ایجاد شده و دوباره نمایش داده نمیشود. اگر پاسخ اصلی گم شده است، کلید را لغو و دوباره ایجاد کنید یا در صورت اجازه از QR استفاده کنید.',
|
|
||||||
uncertain:
|
uncertain:
|
||||||
'نتیجه درخواست نامشخص است. برای جلوگیری از تکرار، با همان اطلاعات دوباره تلاش کنید یا ببندید و پیش از ایجاد کلید دیگر فهرست را بررسی کنید.',
|
'نتیجه درخواست نامشخص است. برای جلوگیری از تکرار، با همان اطلاعات دوباره تلاش کنید یا ببندید و پیش از ایجاد کلید دیگر فهرست را بررسی کنید.',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: 'シークレットを表示',
|
||||||
|
hideSecret: 'シークレットを隠す',
|
||||||
|
revealHelp: 'シークレットを安全に保管してください。表示しても無効または期限切れのキーは使用できません。',
|
||||||
|
deleteConfirm:
|
||||||
|
'「{0}」を削除しますか?シークレットは破棄され、キーは一覧から削除されます。新しいリクエストは拒否され、関連ターミナルは切断されます。監査記録は保持され、実行中のタスクは中止されません。この操作は元に戻せません。',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'API Key を管理',
|
manage: 'API Key を管理',
|
||||||
personalEntry: '設定で自分のキー、アクセス条件、APP の QR 連携を管理します。',
|
personalEntry: '設定で自分のキー、アクセス条件、APP の QR 連携を管理します。',
|
||||||
@@ -23,10 +28,6 @@ const message = {
|
|||||||
allowAppBinding: 'APP の QR 連携を許可',
|
allowAppBinding: 'APP の QR 連携を許可',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'QR 連携を無効にすると、新規の連携を受け付けなくなります。連携済みの端末は、キーが無効化、失効、または期限切れになるまで引き続き利用できます。',
|
'QR 連携を無効にすると、新規の連携を受け付けなくなります。連携済みの端末は、キーが無効化、失効、または期限切れになるまで引き続き利用できます。',
|
||||||
saveSecret: '今すぐキーをコピーして保存してください。閉じると通常のキー管理から完全なキーを再表示できません。',
|
|
||||||
saved: 'キーを保存しました',
|
|
||||||
alreadyCreated:
|
|
||||||
'このキーは作成済みで、再表示できません。最初の応答を受け取れなかった場合は失効させて再作成するか、許可されていれば QR を使用してください。',
|
|
||||||
uncertain: 'リクエスト結果が不明です。重複を避けるため同じ設定で再試行するか、閉じて一覧を確認してください。',
|
uncertain: 'リクエスト結果が不明です。重複を避けるため同じ設定で再試行するか、閉じて一覧を確認してください。',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
'旧版キーは従来の API 接続との互換性を維持します。有効化、無効化、リセットはこのキーにのみ適用されます。',
|
'旧版キーは従来の API 接続との互換性を維持します。有効化、無効化、リセットはこのキーにのみ適用されます。',
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: '비밀 키 표시',
|
||||||
|
hideSecret: '비밀 키 숨기기',
|
||||||
|
revealHelp: '비밀 키를 안전하게 보관하세요. 표시해도 비활성화되거나 만료된 키는 사용할 수 없습니다.',
|
||||||
|
deleteConfirm:
|
||||||
|
'“{0}”을(를) 삭제하시겠습니까? 비밀 키가 파기되고 목록에서 제거됩니다. 새 요청은 거부되고 연결된 터미널은 종료됩니다. 감사 기록은 유지되며 실행 중인 작업은 취소되지 않습니다. 이 작업은 되돌릴 수 없습니다.',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'API Key 관리',
|
manage: 'API Key 관리',
|
||||||
personalEntry: '설정에서 내 키, 접근 조건 및 APP QR 연결을 관리합니다.',
|
personalEntry: '설정에서 내 키, 접근 조건 및 APP QR 연결을 관리합니다.',
|
||||||
@@ -23,10 +28,6 @@ const message = {
|
|||||||
allowAppBinding: 'APP QR 연결 허용',
|
allowAppBinding: 'APP QR 연결 허용',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'QR 연결을 비활성화하면 새로운 기기를 연결할 수 없습니다. 이미 연결된 기기는 키가 비활성화, 폐기 또는 만료될 때까지 계속 사용할 수 있습니다.',
|
'QR 연결을 비활성화하면 새로운 기기를 연결할 수 없습니다. 이미 연결된 기기는 키가 비활성화, 폐기 또는 만료될 때까지 계속 사용할 수 있습니다.',
|
||||||
saveSecret: '지금 키를 복사하여 저장하세요. 닫으면 일반 키 관리에서 전체 키를 다시 표시할 수 없습니다.',
|
|
||||||
saved: '키를 저장했습니다',
|
|
||||||
alreadyCreated:
|
|
||||||
'이 키는 이미 생성되어 다시 표시할 수 없습니다. 원래 응답을 잃었다면 폐기 후 다시 만들거나 허용된 QR 연결을 사용하세요.',
|
|
||||||
uncertain:
|
uncertain:
|
||||||
'요청 결과를 확인할 수 없습니다. 중복 생성을 피하려면 같은 설정으로 재시도하거나 닫고 목록을 확인하세요.',
|
'요청 결과를 확인할 수 없습니다. 중복 생성을 피하려면 같은 설정으로 재시도하거나 닫고 목록을 확인하세요.',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: 'ສະແດງຄີລັບ',
|
||||||
|
hideSecret: 'ເຊື່ອງຄີລັບ',
|
||||||
|
revealHelp: 'ກະລຸນາເກັບຄີລັບໃຫ້ປອດໄພ. ການສະແດງບໍ່ເຮັດໃຫ້ຄີທີ່ປິດໃຊ້ງານ ຫຼືໝົດອາຍຸກັບມາໃຊ້ໄດ້.',
|
||||||
|
deleteConfirm:
|
||||||
|
'ລຶບ “{0}” ຫຼືບໍ່? ຄີລັບຈະຖືກທຳລາຍ ແລະລຶບອອກຈາກລາຍການ. ຄຳຮ້ອງຂໍໃໝ່ຈະຖືກປະຕິເສດ ແລະປິດເທີມິນັລທີ່ກ່ຽວຂ້ອງ. ບັນທຶກການກວດສອບຈະຍັງຄົງຢູ່; ວຽກທີ່ກຳລັງເຮັດຈະບໍ່ຖືກຍົກເລີກ. ບໍ່ສາມາດກູ້ຄືນໄດ້.',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'ຈັດການ API Key',
|
manage: 'ຈັດການ API Key',
|
||||||
personalEntry: 'ຈັດການກະແຈ, ເງື່ອນໄຂເຂົ້າໃຊ້ ແລະ ການຜູກ APP ໃນການຕັ້ງຄ່າ.',
|
personalEntry: 'ຈັດການກະແຈ, ເງື່ອນໄຂເຂົ້າໃຊ້ ແລະ ການຜູກ APP ໃນການຕັ້ງຄ່າ.',
|
||||||
@@ -23,9 +28,6 @@ const message = {
|
|||||||
allowAppBinding: 'ອະນຸຍາດຜູກ APP ຜ່ານ QR',
|
allowAppBinding: 'ອະນຸຍາດຜູກ APP ຜ່ານ QR',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'ເມື່ອປິດການຜູກມັດຜ່ານ QR, ລະບົບຈະບໍ່ຮັບການຜູກມັດໃໝ່. ອຸປະກອນທີ່ຜູກມັດແລ້ວຍັງສາມາດໃຊ້ງານໄດ້ ຈົນກວ່າກະແຈຈະຖືກປິດໃຊ້, ຖອນ ຫຼື ໝົດອາຍຸ.',
|
'ເມື່ອປິດການຜູກມັດຜ່ານ QR, ລະບົບຈະບໍ່ຮັບການຜູກມັດໃໝ່. ອຸປະກອນທີ່ຜູກມັດແລ້ວຍັງສາມາດໃຊ້ງານໄດ້ ຈົນກວ່າກະແຈຈະຖືກປິດໃຊ້, ຖອນ ຫຼື ໝົດອາຍຸ.',
|
||||||
saveSecret: 'ຄັດລອກ ແລະ ບັນທຶກກະແຈດຽວນີ້. ຫຼັງປິດແລ້ວ ການຈັດການປົກກະຕິຈະບໍ່ສະແດງກະແຈເຕັມອີກ.',
|
|
||||||
saved: 'ຂ້ອຍບັນທຶກກະແຈແລ້ວ',
|
|
||||||
alreadyCreated: 'ກະແຈນີ້ສ້າງແລ້ວ ແລະ ບໍ່ສະແດງອີກ. ຖ້າຄຳຕອບເກົ່າຫາຍ ໃຫ້ຖອນແລ້ວສ້າງໃໝ່ ຫຼື ໃຊ້ QR ຖ້າອະນຸຍາດ.',
|
|
||||||
uncertain: 'ຍັງບໍ່ແນ່ໃຈຜົນຄຳຮ້ອງ. ລອງໃໝ່ດ້ວຍຂໍ້ມູນເດີມເພື່ອບໍ່ສ້າງຊ້ຳ ຫຼື ປິດແລ້ວກວດລາຍການກ່ອນສ້າງອີກ.',
|
uncertain: 'ຍັງບໍ່ແນ່ໃຈຜົນຄຳຮ້ອງ. ລອງໃໝ່ດ້ວຍຂໍ້ມູນເດີມເພື່ອບໍ່ສ້າງຊ້ຳ ຫຼື ປິດແລ້ວກວດລາຍການກ່ອນສ້າງອີກ.',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
'ກະແຈເກົ່າໃຊ້ເພື່ອຮອງຮັບການເຊື່ອມຕໍ່ API ເກົ່າ. ການເປີດໃຊ້, ປິດໃຊ້ ແລະ ຣີເຊັດ ມີຜົນສະເພາະກະແຈນີ້ເທົ່ານັ້ນ.',
|
'ກະແຈເກົ່າໃຊ້ເພື່ອຮອງຮັບການເຊື່ອມຕໍ່ API ເກົ່າ. ການເປີດໃຊ້, ປິດໃຊ້ ແລະ ຣີເຊັດ ມີຜົນສະເພາະກະແຈນີ້ເທົ່ານັ້ນ.',
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: 'Papar rahsia',
|
||||||
|
hideSecret: 'Sembunyikan rahsia',
|
||||||
|
revealHelp:
|
||||||
|
'Simpan rahsia ini dengan selamat. Memaparkannya tidak mengaktifkan semula kunci yang dinyahdayakan atau tamat tempoh.',
|
||||||
|
deleteConfirm:
|
||||||
|
'Padam “{0}”? Rahsianya akan dimusnahkan dan kunci dibuang daripada senarai. Permintaan baharu akan ditolak dan terminal berkaitan ditutup. Rekod audit dikekalkan; tugas yang sedang berjalan tidak dibatalkan. Tindakan ini tidak boleh dibuat asal.',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'Urus kunci API',
|
manage: 'Urus kunci API',
|
||||||
personalEntry: 'Urus kunci, syarat akses dan pautan APP anda dalam Tetapan.',
|
personalEntry: 'Urus kunci, syarat akses dan pautan APP anda dalam Tetapan.',
|
||||||
@@ -23,11 +29,6 @@ const message = {
|
|||||||
allowAppBinding: 'Benarkan pautan APP melalui QR',
|
allowAppBinding: 'Benarkan pautan APP melalui QR',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'Menyahdayakan pautan QR menghalang pautan peranti baharu. Peranti yang telah dipautkan boleh terus digunakan sehingga kunci dinyahdayakan, dibatalkan atau tamat tempoh.',
|
'Menyahdayakan pautan QR menghalang pautan peranti baharu. Peranti yang telah dipautkan boleh terus digunakan sehingga kunci dinyahdayakan, dibatalkan atau tamat tempoh.',
|
||||||
saveSecret:
|
|
||||||
'Salin dan simpan kunci sekarang. Selepas ditutup, pengurusan biasa tidak dapat memaparkan kunci penuh lagi.',
|
|
||||||
saved: 'Saya telah menyimpan kunci',
|
|
||||||
alreadyCreated:
|
|
||||||
'Kunci ini telah dicipta dan tidak akan dipaparkan semula. Jika respons asal hilang, batalkan dan cipta semula atau gunakan QR jika dibenarkan.',
|
|
||||||
uncertain:
|
uncertain:
|
||||||
'Hasil permintaan belum pasti. Cuba semula dengan butiran sama untuk mengelakkan pendua, atau tutup dan semak senarai sebelum mencipta lagi.',
|
'Hasil permintaan belum pasti. Cuba semula dengan butiran sama untuk mengelakkan pendua, atau tutup dan semak senarai sebelum mencipta lagi.',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: 'Mostrar segredo',
|
||||||
|
hideSecret: 'Ocultar segredo',
|
||||||
|
revealHelp: 'Guarde o segredo com segurança. Exibi-lo não reativa chaves desabilitadas ou expiradas.',
|
||||||
|
deleteConfirm:
|
||||||
|
'Excluir “{0}”? O segredo será destruído e a chave removida da lista. Novas solicitações serão rejeitadas e os terminais associados serão fechados. Os registros de auditoria serão mantidos; tarefas em execução não serão canceladas. Esta ação é irreversível.',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'Gerenciar chaves API',
|
manage: 'Gerenciar chaves API',
|
||||||
personalEntry: 'Gerencie suas chaves, condições de acesso e vínculo do APP nas Configurações.',
|
personalEntry: 'Gerencie suas chaves, condições de acesso e vínculo do APP nas Configurações.',
|
||||||
@@ -23,10 +28,6 @@ const message = {
|
|||||||
allowAppBinding: 'Permitir vínculo do APP por QR',
|
allowAppBinding: 'Permitir vínculo do APP por QR',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'Desativar a vinculação por QR impede novas vinculações. Os dispositivos já vinculados podem continuar usando a chave até que ela seja desativada, revogada ou expire.',
|
'Desativar a vinculação por QR impede novas vinculações. Os dispositivos já vinculados podem continuar usando a chave até que ela seja desativada, revogada ou expire.',
|
||||||
saveSecret: 'Copie e salve a chave agora. Após fechar, o gerenciamento normal não poderá exibi-la novamente.',
|
|
||||||
saved: 'Salvei a chave',
|
|
||||||
alreadyCreated:
|
|
||||||
'Esta chave já foi criada e não será exibida novamente. Se a resposta original foi perdida, revogue e recrie a chave ou use o QR, se permitido.',
|
|
||||||
uncertain:
|
uncertain:
|
||||||
'O resultado é incerto. Tente novamente com os mesmos dados para evitar duplicações ou feche e verifique a lista antes de criar outra.',
|
'O resultado é incerto. Tente novamente com os mesmos dados para evitar duplicações ou feche e verifique a lista antes de criar outra.',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: 'Показать секрет',
|
||||||
|
hideSecret: 'Скрыть секрет',
|
||||||
|
revealHelp: 'Храните секрет в безопасности. Просмотр не активирует отключённые или просроченные ключи.',
|
||||||
|
deleteConfirm:
|
||||||
|
'Удалить «{0}»? Секрет будет уничтожен, а ключ удалён из списка. Новые запросы будут отклонены, связанные терминалы закрыты. Журнал аудита сохранится; выполняющиеся задачи не отменяются. Действие необратимо.',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'Управление API-ключами',
|
manage: 'Управление API-ключами',
|
||||||
personalEntry: 'Управляйте своими ключами, условиями доступа и привязкой приложения в настройках.',
|
personalEntry: 'Управляйте своими ключами, условиями доступа и привязкой приложения в настройках.',
|
||||||
@@ -23,11 +28,6 @@ const message = {
|
|||||||
allowAppBinding: 'Разрешить привязку приложения по QR',
|
allowAppBinding: 'Разрешить привязку приложения по QR',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'Отключение привязки по QR-коду запрещает новые привязки. Уже привязанные устройства могут продолжать работу, пока ключ не будет отключён, отозван или не истечёт срок его действия.',
|
'Отключение привязки по QR-коду запрещает новые привязки. Уже привязанные устройства могут продолжать работу, пока ключ не будет отключён, отозван или не истечёт срок его действия.',
|
||||||
saveSecret:
|
|
||||||
'Скопируйте и сохраните ключ сейчас. После закрытия обычное управление не сможет показать полный ключ снова.',
|
|
||||||
saved: 'Ключ сохранён',
|
|
||||||
alreadyCreated:
|
|
||||||
'Ключ уже создан и не будет показан повторно. Если исходный ответ потерян, отзовите и создайте ключ заново либо используйте разрешённую привязку по QR.',
|
|
||||||
uncertain:
|
uncertain:
|
||||||
'Результат запроса неизвестен. Повторите с теми же данными, чтобы избежать дублей, или закройте и проверьте список.',
|
'Результат запроса неизвестен. Повторите с теми же данными, чтобы избежать дублей, или закройте и проверьте список.',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: 'Gizli anahtarı göster',
|
||||||
|
hideSecret: 'Gizli anahtarı gizle',
|
||||||
|
revealHelp:
|
||||||
|
'Gizli anahtarı güvenli tutun. Görüntülemek, devre dışı veya süresi dolmuş anahtarı kullanılabilir yapmaz.',
|
||||||
|
deleteConfirm:
|
||||||
|
'“{0}” silinsin mi? Gizli anahtar yok edilecek ve listeden kaldırılacaktır. Yeni istekler reddedilecek ve ilgili terminaller kapatılacaktır. Denetim kayıtları korunur; çalışan işler iptal edilmez. Bu işlem geri alınamaz.',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: 'API anahtarlarını yönet',
|
manage: 'API anahtarlarını yönet',
|
||||||
personalEntry: 'Ayarlar bölümünde anahtarlarınızı, erişim koşullarını ve uygulama QR bağlantısını yönetin.',
|
personalEntry: 'Ayarlar bölümünde anahtarlarınızı, erişim koşullarını ve uygulama QR bağlantısını yönetin.',
|
||||||
@@ -23,11 +29,6 @@ const message = {
|
|||||||
allowAppBinding: 'Uygulama QR bağlantısına izin ver',
|
allowAppBinding: 'Uygulama QR bağlantısına izin ver',
|
||||||
bindingDisableHelp:
|
bindingDisableHelp:
|
||||||
'QR eşleştirmesini devre dışı bırakmak yeni eşleştirmeleri engeller. Eşleştirilmiş cihazlar, anahtar devre dışı bırakılana, iptal edilene veya süresi dolana kadar kullanılmaya devam edebilir.',
|
'QR eşleştirmesini devre dışı bırakmak yeni eşleştirmeleri engeller. Eşleştirilmiş cihazlar, anahtar devre dışı bırakılana, iptal edilene veya süresi dolana kadar kullanılmaya devam edebilir.',
|
||||||
saveSecret:
|
|
||||||
'Anahtarı şimdi kopyalayıp kaydedin. Kapattıktan sonra normal anahtar yönetimi tam anahtarı yeniden gösteremez.',
|
|
||||||
saved: 'Anahtarı kaydettim',
|
|
||||||
alreadyCreated:
|
|
||||||
'Bu anahtar zaten oluşturuldu ve yeniden gösterilmez. İlk yanıt kaybolduysa iptal edip yeniden oluşturun veya izin veriliyorsa QR bağlantısını kullanın.',
|
|
||||||
uncertain:
|
uncertain:
|
||||||
'İstek sonucu belirsiz. Tekrarları önlemek için aynı bilgilerle yeniden deneyin veya kapatıp yeni anahtar oluşturmadan listeyi kontrol edin.',
|
'İstek sonucu belirsiz. Tekrarları önlemek için aynı bilgilerle yeniden deneyin veya kapatıp yeni anahtar oluşturmadan listeyi kontrol edin.',
|
||||||
legacyHelp:
|
legacyHelp:
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: '檢視密鑰',
|
||||||
|
hideSecret: '隱藏密鑰',
|
||||||
|
revealHelp: '請妥善保管密鑰。檢視不會恢復已停用或已過期密鑰的使用權限。',
|
||||||
|
deleteConfirm:
|
||||||
|
'確定刪除「{0}」嗎?密鑰將被銷毀並從列表移除,拒絕後續請求並關閉關聯終端。稽核記錄仍保留,執行中的工作不會取消,此操作無法復原。',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: '管理 API Key',
|
manage: '管理 API Key',
|
||||||
personalEntry: '前往設定管理自己的金鑰、存取條件與 APP 掃碼綁定。',
|
personalEntry: '前往設定管理自己的金鑰、存取條件與 APP 掃碼綁定。',
|
||||||
@@ -22,10 +27,6 @@ const message = {
|
|||||||
futureExpiry: '請選擇晚於目前時間的到期時間。',
|
futureExpiry: '請選擇晚於目前時間的到期時間。',
|
||||||
allowAppBinding: '允許 APP 掃碼綁定',
|
allowAppBinding: '允許 APP 掃碼綁定',
|
||||||
bindingDisableHelp: '關閉掃碼綁定後,將停止接受新的綁定;已綁定裝置可繼續使用,直至金鑰被停用、撤銷或到期。',
|
bindingDisableHelp: '關閉掃碼綁定後,將停止接受新的綁定;已綁定裝置可繼續使用,直至金鑰被停用、撤銷或到期。',
|
||||||
saveSecret: '請立即複製並儲存金鑰。關閉後,一般金鑰管理無法再次顯示完整金鑰。',
|
|
||||||
saved: '我已儲存金鑰',
|
|
||||||
alreadyCreated:
|
|
||||||
'這把金鑰已建立,無法再次顯示明文。若原回應遺失,請撤銷後重新建立;允許掃碼綁定時,也可透過掃碼交付。',
|
|
||||||
uncertain: '請求結果尚不確定。請保持原設定重試,避免重複建立;或關閉並檢查清單後再建立其他金鑰。',
|
uncertain: '請求結果尚不確定。請保持原設定重試,避免重複建立;或關閉並檢查清單後再建立其他金鑰。',
|
||||||
legacyHelp: '舊版 Key 用於相容歷史 API 連線,啟用、停用及重設操作僅對該 Key 生效。',
|
legacyHelp: '舊版 Key 用於相容歷史 API 連線,啟用、停用及重設操作僅對該 Key 生效。',
|
||||||
showLegacy: '檢視舊版金鑰',
|
showLegacy: '檢視舊版金鑰',
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { getFuLocaleMessage } from '@/lang/fu';
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
apiKeyManagement: {
|
apiKeyManagement: {
|
||||||
|
reveal: '查看密钥',
|
||||||
|
hideSecret: '隐藏密钥',
|
||||||
|
revealHelp: '请妥善保管密钥。查看不会恢复已停用或已过期密钥的使用权限。',
|
||||||
|
deleteConfirm:
|
||||||
|
'确定删除“{0}”吗?密钥将被销毁并从列表移除,拒绝后续请求并关闭关联终端。审计记录仍保留,运行中的任务不会取消,此操作不可恢复。',
|
||||||
title: 'API Key',
|
title: 'API Key',
|
||||||
manage: '管理 API Key',
|
manage: '管理 API Key',
|
||||||
personalEntry: '前往设置管理自己的密钥、访问条件和 APP 扫码绑定。',
|
personalEntry: '前往设置管理自己的密钥、访问条件和 APP 扫码绑定。',
|
||||||
@@ -22,10 +27,6 @@ const message = {
|
|||||||
futureExpiry: '请选择晚于当前时间的到期时间。',
|
futureExpiry: '请选择晚于当前时间的到期时间。',
|
||||||
allowAppBinding: '允许 APP 扫码绑定',
|
allowAppBinding: '允许 APP 扫码绑定',
|
||||||
bindingDisableHelp: '关闭扫码绑定后,将停止接受新的绑定;已绑定设备可继续使用,直至密钥被停用、撤销或到期。',
|
bindingDisableHelp: '关闭扫码绑定后,将停止接受新的绑定;已绑定设备可继续使用,直至密钥被停用、撤销或到期。',
|
||||||
saveSecret: '请立即复制并保存密钥。关闭后,普通密钥管理无法再次显示完整密钥。',
|
|
||||||
saved: '我已保存密钥',
|
|
||||||
alreadyCreated:
|
|
||||||
'这把密钥已创建,无法再次展示明文。若原响应丢失,请撤销后重新创建;允许扫码绑定时,也可通过扫码交付。',
|
|
||||||
uncertain: '请求结果尚不确定。请保持原配置重试,避免重复创建;或关闭并检查列表后再创建其他密钥。',
|
uncertain: '请求结果尚不确定。请保持原配置重试,避免重复创建;或关闭并检查列表后再创建其他密钥。',
|
||||||
legacyHelp: '旧版 Key 用于兼容历史 API 连接,启用、停用和重置操作仅对该 Key 生效。',
|
legacyHelp: '旧版 Key 用于兼容历史 API 连接,启用、停用和重置操作仅对该 Key 生效。',
|
||||||
showLegacy: '查看旧版密钥',
|
showLegacy: '查看旧版密钥',
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500 break-all">{{ row.keyHint }}</div>
|
<div class="text-xs text-[var(--el-text-color-secondary)] break-all">{{ row.keyHint }}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('commons.table.status')" min-width="100" prop="status">
|
<el-table-column :label="$t('commons.table.status')" min-width="100" prop="status">
|
||||||
@@ -137,26 +137,50 @@
|
|||||||
v-model="detailVisible"
|
v-model="detailVisible"
|
||||||
:header="$t('apiKeyManagement.details')"
|
:header="$t('apiKeyManagement.details')"
|
||||||
size="min(640px, 100vw)"
|
size="min(640px, 100vw)"
|
||||||
@close="clearLegacySecret"
|
@close="clearSecret"
|
||||||
>
|
>
|
||||||
<APIKeySummary v-if="detail" :item="detail" />
|
<APIKeySummary v-if="detail" :item="detail" />
|
||||||
<div v-if="detail?.kind === 'legacy' && detail.status !== 'Revoked'" class="mt-5 flex flex-col gap-3">
|
<div v-if="detail && detail.status !== 'Revoked'" class="mt-5 flex flex-col gap-3">
|
||||||
<el-alert :title="$t('apiKeyManagement.legacyHelp')" type="info" :closable="false" show-icon />
|
<el-alert
|
||||||
<el-space wrap>
|
:title="
|
||||||
<el-button type="primary" plain :loading="legacyBusy" @click="revealLegacy">
|
$t(detail.kind === 'legacy' ? 'apiKeyManagement.legacyHelp' : 'apiKeyManagement.revealHelp')
|
||||||
{{ $t('apiKeyManagement.showLegacy') }}
|
"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-space v-if="detail.kind === 'legacy'" wrap>
|
||||||
|
<el-button :disabled="secretBusy" @click="resetLegacy">
|
||||||
|
{{ $t('setting.apiKeyReset') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button :disabled="legacyBusy" @click="resetLegacy">{{ $t('setting.apiKeyReset') }}</el-button>
|
|
||||||
</el-space>
|
</el-space>
|
||||||
<el-input
|
<el-input
|
||||||
v-if="legacySecret"
|
:model-value="secret || '********'"
|
||||||
:model-value="legacySecret"
|
:type="secret ? 'text' : 'password'"
|
||||||
:aria-label="$t('setting.apiKey')"
|
:aria-label="$t('setting.apiKey')"
|
||||||
readonly
|
readonly
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
>
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
:icon="secret ? Hide : View"
|
||||||
|
:loading="secretBusy && secretAction === 'reveal'"
|
||||||
|
:disabled="loading || secretBusy"
|
||||||
|
:aria-label="$t(secret ? 'apiKeyManagement.hideSecret' : 'apiKeyManagement.reveal')"
|
||||||
|
:title="$t(secret ? 'apiKeyManagement.hideSecret' : 'apiKeyManagement.reveal')"
|
||||||
|
:aria-pressed="!!secret"
|
||||||
|
@click="toggleSecret"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<CopyButton :content="legacySecret" :is-icon="false" />
|
<el-button
|
||||||
|
:loading="secretBusy && secretAction === 'copy'"
|
||||||
|
:disabled="loading || secretBusy"
|
||||||
|
@click="copySecret"
|
||||||
|
>
|
||||||
|
{{ $t('commons.button.copy') }}
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,10 +189,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, reactive, ref } from 'vue';
|
import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { ElMessageBox } from 'element-plus';
|
import { ElMessageBox } from 'element-plus';
|
||||||
|
import { Hide, View } from '@element-plus/icons-vue';
|
||||||
import type { APIKey } from '@/api/interface/api-key';
|
import type { APIKey } from '@/api/interface/api-key';
|
||||||
import { revokeAPIKey, searchAPIKeys, setAPIKeyStatus } from '@/api/modules/api-key';
|
import { deleteAPIKey, revealAPIKey, revokeAPIKey, searchAPIKeys, setAPIKeyStatus } from '@/api/modules/api-key';
|
||||||
import { generateApiKey, getUserInfo } from '@/api/modules/auth';
|
import { generateApiKey, getUserInfo } from '@/api/modules/auth';
|
||||||
import APIKeyEditor from '@/components/api-key-management/editor.vue';
|
import APIKeyEditor from '@/components/api-key-management/editor.vue';
|
||||||
import APIKeySummary from '@/components/api-key-management/summary.vue';
|
import APIKeySummary from '@/components/api-key-management/summary.vue';
|
||||||
@@ -176,35 +201,38 @@ import DrawerPro from '@/components/drawer-pro/index.vue';
|
|||||||
import type { FuTableOperationButton } from '@/components/table/shared';
|
import type { FuTableOperationButton } from '@/components/table/shared';
|
||||||
import { isAnyAPIKeyIP, normalizeAPIKeyIPs } from '@/utils/api-key';
|
import { isAnyAPIKeyIP, normalizeAPIKeyIPs } from '@/utils/api-key';
|
||||||
import { MsgSuccess, MsgWarning } from '@/utils/message';
|
import { MsgSuccess, MsgWarning } from '@/utils/message';
|
||||||
|
import { copyText } from '@/utils/clipboard';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||||
|
|
||||||
const { isMobile } = useGlobalStore();
|
const { isMobile, globalStore } = useGlobalStore();
|
||||||
|
|
||||||
const items = ref<APIKey.Item[]>([]);
|
const items = ref<APIKey.Item[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const editor = ref<InstanceType<typeof APIKeyEditor>>();
|
const editor = ref<InstanceType<typeof APIKeyEditor>>();
|
||||||
const detail = ref<APIKey.Item>();
|
const detail = ref<APIKey.Item>();
|
||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
const legacySecret = ref('');
|
const secret = ref('');
|
||||||
const legacyBusy = ref(false);
|
const secretBusy = ref(false);
|
||||||
|
const secretAction = ref<'reveal' | 'copy'>('reveal');
|
||||||
const pagination = reactive({ currentPage: 1, pageSize: 20, total: 0 });
|
const pagination = reactive({ currentPage: 1, pageSize: 20, total: 0 });
|
||||||
let searchVersion = 0;
|
let searchVersion = 0;
|
||||||
let secretRequestVersion = 0;
|
let secretRequestVersion = 0;
|
||||||
let active = false;
|
let active = false;
|
||||||
|
let operationPending = false;
|
||||||
|
|
||||||
const whitelistEntries = (value: string) =>
|
const whitelistEntries = (value: string) =>
|
||||||
normalizeAPIKeyIPs(value || '')
|
normalizeAPIKeyIPs(value || '')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const clearLegacySecret = () => {
|
const clearSecret = () => {
|
||||||
secretRequestVersion++;
|
secretRequestVersion++;
|
||||||
legacySecret.value = '';
|
secret.value = '';
|
||||||
legacyBusy.value = false;
|
secretBusy.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = async (preserveLegacyRequest?: number) => {
|
const search = async (preserveSecretRequest?: number) => {
|
||||||
const ticket = ++searchVersion;
|
const ticket = ++searchVersion;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -213,7 +241,7 @@ const search = async (preserveLegacyRequest?: number) => {
|
|||||||
const lastPage = Math.max(1, Math.ceil(response.data.total / pagination.pageSize));
|
const lastPage = Math.max(1, Math.ceil(response.data.total / pagination.pageSize));
|
||||||
if (pagination.currentPage > lastPage) {
|
if (pagination.currentPage > lastPage) {
|
||||||
pagination.currentPage = lastPage;
|
pagination.currentPage = lastPage;
|
||||||
await search(preserveLegacyRequest);
|
await search(preserveSecretRequest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
items.value = response.data.items || [];
|
items.value = response.data.items || [];
|
||||||
@@ -221,8 +249,8 @@ const search = async (preserveLegacyRequest?: number) => {
|
|||||||
if (detail.value) {
|
if (detail.value) {
|
||||||
const previousRevision = detail.value.revision;
|
const previousRevision = detail.value.revision;
|
||||||
detail.value = items.value.find((entry) => entry.id === detail.value?.id);
|
detail.value = items.value.find((entry) => entry.id === detail.value?.id);
|
||||||
if (detail.value?.revision !== previousRevision && preserveLegacyRequest !== secretRequestVersion)
|
if (detail.value?.revision !== previousRevision && preserveSecretRequest !== secretRequestVersion)
|
||||||
clearLegacySecret();
|
clearSecret();
|
||||||
if (!detail.value) detailVisible.value = false;
|
if (!detail.value) detailVisible.value = false;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -230,7 +258,7 @@ const search = async (preserveLegacyRequest?: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const showDetail = (entry: APIKey.Item) => {
|
const showDetail = (entry: APIKey.Item) => {
|
||||||
clearLegacySecret();
|
clearSecret();
|
||||||
detail.value = entry;
|
detail.value = entry;
|
||||||
detailVisible.value = true;
|
detailVisible.value = true;
|
||||||
};
|
};
|
||||||
@@ -239,11 +267,15 @@ const operate = async (entry: APIKey.Item, action: string) => {
|
|||||||
editor.value?.open(entry);
|
editor.value?.open(entry);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action !== 'status' && action !== 'revoke') return;
|
if (!active || operationPending || !['status', 'revoke', 'delete'].includes(action)) return;
|
||||||
|
operationPending = true;
|
||||||
|
const requestVersion = searchVersion;
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
i18n.global.t(
|
i18n.global.t(
|
||||||
action === 'revoke'
|
action === 'delete'
|
||||||
|
? 'apiKeyManagement.deleteConfirm'
|
||||||
|
: action === 'revoke'
|
||||||
? 'apiKeyManagement.revokeConfirm'
|
? 'apiKeyManagement.revokeConfirm'
|
||||||
: entry.status === 'Enable'
|
: entry.status === 'Enable'
|
||||||
? 'apiKeyManagement.disableConfirm'
|
? 'apiKeyManagement.disableConfirm'
|
||||||
@@ -258,20 +290,29 @@ const operate = async (entry: APIKey.Item, action: string) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
|
operationPending = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!active || requestVersion !== searchVersion) {
|
||||||
|
operationPending = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearSecret();
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const reference = { id: entry.id, revision: entry.revision };
|
const reference = { id: entry.id, revision: entry.revision };
|
||||||
const response =
|
const response =
|
||||||
action === 'revoke'
|
action === 'delete'
|
||||||
|
? await deleteAPIKey(reference)
|
||||||
|
: action === 'revoke'
|
||||||
? await revokeAPIKey(reference)
|
? await revokeAPIKey(reference)
|
||||||
: await setAPIKeyStatus({ ...reference, status: entry.status === 'Enable' ? 'Disable' : 'Enable' });
|
: await setAPIKeyStatus({ ...reference, status: entry.status === 'Enable' ? 'Disable' : 'Enable' });
|
||||||
clearLegacySecret();
|
clearSecret();
|
||||||
if (response.data?.terminalClosePending) MsgWarning(i18n.global.t('apiKeyManagement.closePending'));
|
if (response.data?.terminalClosePending) MsgWarning(i18n.global.t('apiKeyManagement.closePending'));
|
||||||
else MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
else MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
} finally {
|
} finally {
|
||||||
await search();
|
operationPending = false;
|
||||||
|
if (active) await search();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const buttons = computed<FuTableOperationButton<APIKey.Item>[]>(() => [
|
const buttons = computed<FuTableOperationButton<APIKey.Item>[]>(() => [
|
||||||
@@ -300,21 +341,59 @@ const buttons = computed<FuTableOperationButton<APIKey.Item>[]>(() => [
|
|||||||
show: (row) => row.status !== 'Revoked',
|
show: (row) => row.status !== 'Revoked',
|
||||||
click: (row) => operate(row, 'revoke'),
|
click: (row) => operate(row, 'revoke'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: i18n.global.t('commons.button.delete'),
|
||||||
|
show: (row) => row.kind === 'apiKey',
|
||||||
|
click: (row) => operate(row, 'delete'),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
const revealLegacy = async () => {
|
const toggleSecret = async () => {
|
||||||
if (legacyBusy.value || !detailVisible.value || detail.value?.kind !== 'legacy') return;
|
if (secretBusy.value || loading.value) return;
|
||||||
|
if (secret.value) clearSecret();
|
||||||
|
else await revealSecret();
|
||||||
|
};
|
||||||
|
const accessSecret = async (action: 'reveal' | 'copy') => {
|
||||||
|
if (secretBusy.value || loading.value || !active || !detailVisible.value || !detail.value) return;
|
||||||
|
if (detail.value.status === 'Revoked') return;
|
||||||
|
const reference = { id: detail.value.id, revision: detail.value.revision };
|
||||||
|
const kind = detail.value.kind;
|
||||||
const ticket = ++secretRequestVersion;
|
const ticket = ++secretRequestVersion;
|
||||||
legacyBusy.value = true;
|
secretAction.value = action;
|
||||||
|
secretBusy.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await getUserInfo();
|
if (action === 'copy' && secret.value) {
|
||||||
if (ticket === secretRequestVersion && detailVisible.value && active && detail.value?.kind === 'legacy')
|
await copyText(secret.value);
|
||||||
legacySecret.value = response.data.apiKey;
|
return;
|
||||||
|
}
|
||||||
|
secret.value = '';
|
||||||
|
const data =
|
||||||
|
kind === 'legacy'
|
||||||
|
? { ...reference, apiKey: (await getUserInfo()).data.apiKey }
|
||||||
|
: (await revealAPIKey(reference)).data;
|
||||||
|
if (
|
||||||
|
ticket === secretRequestVersion &&
|
||||||
|
active &&
|
||||||
|
detailVisible.value &&
|
||||||
|
detail.value?.id === reference.id &&
|
||||||
|
detail.value?.kind === kind &&
|
||||||
|
detail.value?.revision === reference.revision &&
|
||||||
|
data.id === reference.id &&
|
||||||
|
data.revision === reference.revision &&
|
||||||
|
data.apiKey
|
||||||
|
) {
|
||||||
|
if (action === 'copy') await copyText(data.apiKey);
|
||||||
|
else secret.value = data.apiKey;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (ticket === secretRequestVersion && active) await search();
|
||||||
} finally {
|
} finally {
|
||||||
if (ticket === secretRequestVersion) legacyBusy.value = false;
|
if (ticket === secretRequestVersion) secretBusy.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const revealSecret = () => accessSecret('reveal');
|
||||||
|
const copySecret = () => accessSecret('copy');
|
||||||
const resetLegacy = async () => {
|
const resetLegacy = async () => {
|
||||||
if (legacyBusy.value || !detailVisible.value || detail.value?.kind !== 'legacy') return;
|
if (secretBusy.value || !detailVisible.value || detail.value?.kind !== 'legacy') return;
|
||||||
const ticket = ++secretRequestVersion;
|
const ticket = ++secretRequestVersion;
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(i18n.global.t('setting.apiKeyResetHelper'), i18n.global.t('setting.apiKeyReset'));
|
await ElMessageBox.confirm(i18n.global.t('setting.apiKeyResetHelper'), i18n.global.t('setting.apiKeyReset'));
|
||||||
@@ -322,17 +401,18 @@ const resetLegacy = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ticket !== secretRequestVersion || !detailVisible.value || !active) return;
|
if (ticket !== secretRequestVersion || !detailVisible.value || !active) return;
|
||||||
legacyBusy.value = true;
|
secretAction.value = 'reveal';
|
||||||
legacySecret.value = '';
|
secretBusy.value = true;
|
||||||
|
secret.value = '';
|
||||||
try {
|
try {
|
||||||
const response = await generateApiKey();
|
const response = await generateApiKey();
|
||||||
await search(ticket);
|
await search(ticket);
|
||||||
// A reset changes the revision and deliberately clears any older reveal.
|
// A reset changes the revision and deliberately clears any older reveal.
|
||||||
// Never restore a secret after the original details drawer was closed.
|
// Never restore a secret after the original details drawer was closed.
|
||||||
if (ticket === secretRequestVersion && detailVisible.value && active && detail.value?.kind === 'legacy')
|
if (ticket === secretRequestVersion && detailVisible.value && active && detail.value?.kind === 'legacy')
|
||||||
legacySecret.value = response.data;
|
secret.value = response.data;
|
||||||
} finally {
|
} finally {
|
||||||
if (ticket === secretRequestVersion) legacyBusy.value = false;
|
if (ticket === secretRequestVersion) secretBusy.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const activate = () => {
|
const activate = () => {
|
||||||
@@ -343,9 +423,16 @@ const activate = () => {
|
|||||||
const deactivate = () => {
|
const deactivate = () => {
|
||||||
active = false;
|
active = false;
|
||||||
searchVersion++;
|
searchVersion++;
|
||||||
clearLegacySecret();
|
clearSecret();
|
||||||
detailVisible.value = false;
|
detailVisible.value = false;
|
||||||
};
|
};
|
||||||
|
watch(
|
||||||
|
() => globalStore.isLogin,
|
||||||
|
(loggedIn) => {
|
||||||
|
if (!loggedIn) deactivate();
|
||||||
|
},
|
||||||
|
{ flush: 'sync' },
|
||||||
|
);
|
||||||
onMounted(activate);
|
onMounted(activate);
|
||||||
onActivated(activate);
|
onActivated(activate);
|
||||||
onDeactivated(deactivate);
|
onDeactivated(deactivate);
|
||||||
|
|||||||
Reference in New Issue
Block a user