diff --git a/core/app/api/v2/api_key.go b/core/app/api/v2/api_key.go index 288642f6b..d40363b3f 100644 --- a/core/app/api/v2/api_key.go +++ b/core/app/api/v2/api_key.go @@ -15,7 +15,7 @@ import ( ) 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) return false } @@ -41,6 +41,24 @@ func apiKeyError(c *gin.Context, err error) { 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) { var req dto.APIKeySearch if !bindAPIKeyRequest(c, &req) { @@ -55,6 +73,25 @@ func (b *BaseApi) SearchAPIKeys(c *gin.Context) { 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) { req := dto.APIKeyCreate{APIKeyFields: dto.APIKeyFields{APIKeyValidityTime: 120}} if !bindAPIKeyRequest(c, &req) { @@ -69,6 +106,22 @@ func (b *BaseApi) CreateAPIKey(c *gin.Context) { 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) { var req dto.APIKeyUpdate if !bindAPIKeyRequest(c, &req) { @@ -81,6 +134,21 @@ func (b *BaseApi) UpdateAPIKey(c *gin.Context) { 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) { var req dto.APIKeyStatus if !bindAPIKeyRequest(c, &req) { @@ -93,6 +161,22 @@ func (b *BaseApi) SetAPIKeyStatus(c *gin.Context) { 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) { var req dto.APIKeyMutation if !bindAPIKeyRequest(c, &req) { @@ -106,5 +190,63 @@ func (b *BaseApi) RevokeAPIKey(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) } diff --git a/core/app/api/v2/auth.go b/core/app/api/v2/auth.go index 8b2bd98c9..e454519fc 100644 --- a/core/app/api/v2/auth.go +++ b/core/app/api/v2/auth.go @@ -417,11 +417,13 @@ func (b *BaseApi) MFAClose(c *gin.Context) { } // @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 -// @Success 200 {string} key -// @Security ApiKeyAuth -// @Security Timestamp +// @Produce json +// @Success 200 {object} dto.Response{data=string} "code=200 with the new secret; authentication and business errors use envelope code 400, 401 or 500" +// @Header 200 {string} Cache-Control "no-store" +// @Security PanelSession || CSRFToken // @Router /core/auth/api/generate [post] // @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"} func (b *BaseApi) GenerateApiKey(c *gin.Context) { @@ -440,12 +442,13 @@ func (b *BaseApi) GenerateApiKey(c *gin.Context) { } // @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 +// @Produce json // @Param request body dto.ApiInterfaceConfig true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Security Timestamp +// @Success 200 {object} dto.Response "code=200 on success; authentication and business errors use envelope code 400, 401 or 500" +// @Security PanelSession || CSRFToken // @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]"} func (b *BaseApi) UpdateApiConfig(c *gin.Context) { diff --git a/core/app/auth/api_auth.go b/core/app/auth/api_auth.go index 855d0463b..6b9218368 100644 --- a/core/app/auth/api_auth.go +++ b/core/app/auth/api_auth.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "errors" "net" + "net/http" "strconv" "strings" "time" @@ -42,7 +43,7 @@ func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessH ownerProvider = ownerProviders[0] } 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() 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) { settingRepo := repo.NewISettingRepo() config := APIAuthConfig{} diff --git a/core/app/auth/api_key.go b/core/app/auth/api_key.go index 0dbdc99ab..738331d4f 100644 --- a/core/app/auth/api_key.go +++ b/core/app/auth/api_key.go @@ -66,6 +66,26 @@ func RequireAPIKeySession(c *gin.Context) (psession.SessionUser, error) { 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 { 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) { - if key.SecretVersion != 1 || key.SecretCiphertext == "" { + if key.DeletedAt != nil || key.Status == "Revoked" || key.SecretVersion != 1 || key.SecretCiphertext == "" { return "", buserr.New("ErrApiConfigKeyInvalid") } master, err := encrypt.APIKeyEncryptionKey() @@ -98,7 +118,7 @@ func APIKeySecret(key model.APIKey) (string, 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") } 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) { 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 != "" { q = q.Where("id = ?", keyID) } else { diff --git a/core/app/dto/api_key.go b/core/app/dto/api_key.go index 1fec82416..3bd9dad12 100644 --- a/core/app/dto/api_key.go +++ b/core/app/dto/api_key.go @@ -6,41 +6,42 @@ import ( ) type APIKeyItem struct { - ID string `json:"id"` - Kind string `json:"kind"` - Name string `json:"name"` - Description string `json:"description"` - KeyHint string `json:"keyHint"` - Status string `json:"status"` - IPWhiteList string `json:"ipWhiteList"` - APITrustedProxies string `json:"apiTrustedProxies"` - APIKeyValidityTime int `json:"apiKeyValidityTime"` - ExpiresAt *time.Time `json:"expiresAt"` - AllowAppBinding bool `json:"allowAppBinding"` - Revision uint64 `json:"revision"` - CreatedAt *time.Time `json:"createdAt"` + ID string `json:"id" example:"8a8f0f77-1ec8-4d46-8f5a-0ea8807e2d8a"` + Kind string `json:"kind" enums:"apiKey,legacy" example:"apiKey"` + Name string `json:"name" example:"Deployment automation"` + Description string `json:"description" example:"Used by the deployment service"` + KeyHint string `json:"keyHint" example:"abcd••••1234"` + Status string `json:"status" enums:"Enable,Disable,Revoked,Expired" example:"Enable"` + IPWhiteList string `json:"ipWhiteList" example:"192.0.2.10"` + APITrustedProxies string `json:"apiTrustedProxies" example:"192.0.2.1"` + APIKeyValidityTime int `json:"apiKeyValidityTime" example:"120"` + ExpiresAt *time.Time `json:"expiresAt" format:"date-time" extensions:"x-nullable"` + AllowAppBinding bool `json:"allowAppBinding" example:"false"` + Revision uint64 `json:"revision" minimum:"1" example:"1"` + CreatedAt *time.Time `json:"createdAt" format:"date-time" extensions:"x-nullable"` } type APIKeySearch struct { - Page int `json:"page"` - PageSize int `json:"pageSize"` - ExcludeRevoked bool `json:"excludeRevoked"` + Page int `json:"page" minimum:"1" example:"1"` + PageSize int `json:"pageSize" minimum:"1" maximum:"100" example:"20"` + ExcludeRevoked bool `json:"excludeRevoked" default:"false"` } type APIKeyPage struct { Items []APIKeyItem `json:"items"` Total int `json:"total"` } type APIKeyFields struct { - Name string `json:"name"` - Description string `json:"description"` - IPWhiteList string `json:"ipWhiteList"` - APITrustedProxies string `json:"apiTrustedProxies"` - APIKeyValidityTime int `json:"apiKeyValidityTime"` - ExpiresAt json.RawMessage `json:"expiresAt"` - AllowAppBinding bool `json:"allowAppBinding"` + Name string `json:"name" minLength:"1" maxLength:"64" example:"Deployment automation"` + Description string `json:"description" maxLength:"256" example:"Used by the deployment service"` + IPWhiteList string `json:"ipWhiteList" maxLength:"4096" example:"192.0.2.10"` + APITrustedProxies string `json:"apiTrustedProxies" maxLength:"4096" example:"192.0.2.1"` + APIKeyValidityTime int `json:"apiKeyValidityTime" minimum:"0" maximum:"1440" example:"120"` + ExpiresAt json.RawMessage `json:"expiresAt" swaggertype:"string" format:"date-time" extensions:"x-nullable" example:"2030-01-01T00:00:00Z"` + AllowAppBinding bool `json:"allowAppBinding" default:"false"` } 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 } type APIKeyCreated struct { @@ -49,8 +50,15 @@ type APIKeyCreated struct { AlreadyCreated bool `json:"alreadyCreated,omitempty"` } type APIKeyMutation struct { - ID string `json:"id"` - Revision uint64 `json:"revision"` + ID string `json:"id" example:"8a8f0f77-1ec8-4d46-8f5a-0ea8807e2d8a"` + 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 { APIKeyMutation @@ -58,5 +66,5 @@ type APIKeyUpdate struct { } type APIKeyStatus struct { APIKeyMutation - Status string `json:"status"` + Status string `json:"status" enums:"Enable,Disable" example:"Disable"` } diff --git a/core/app/model/api_key.go b/core/app/model/api_key.go index 7fc83384e..3d352bb76 100644 --- a/core/app/model/api_key.go +++ b/core/app/model/api_key.go @@ -24,6 +24,7 @@ type APIKey struct { CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` RevokedAt *time.Time `json:"-"` + DeletedAt *time.Time `json:"-"` } type LegacyAPIKeyPolicy struct { diff --git a/core/app/repo/api_key.go b/core/app/repo/api_key.go index bb88926cd..b5ffdb103 100644 --- a/core/app/repo/api_key.go +++ b/core/app/repo/api_key.go @@ -14,12 +14,12 @@ func APIKeyOwnerQuery(db *gorm.DB, ownerType, ownerID string) *gorm.DB { } func (r *APIKeyRepo) Get(id string) (model.APIKey, error) { 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 } func (r *APIKeyRepo) List(ownerType, ownerID string, excludeRevoked bool) ([]model.APIKey, error) { items := make([]model.APIKey, 0) - q := APIKeyOwnerQuery(global.DB, ownerType, ownerID) + q := APIKeyOwnerQuery(global.DB, ownerType, ownerID).Where("deleted_at IS NULL") if excludeRevoked { q = q.Where("status <> ?", "Revoked") } diff --git a/core/app/service/api_key.go b/core/app/service/api_key.go index ead62d654..9c4dc2039 100644 --- a/core/app/service/api_key.go +++ b/core/app/service/api_key.go @@ -52,6 +52,27 @@ func (s *APIKeyService) owner(c *gin.Context) (auth.APIKeyOwner, error) { 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 { status := key.Status 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) { - owner, err := s.owner(c) + owner, err := s.managementOwner(c) if err != nil { 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) { - owner, err := s.owner(c) + owner, err := s.managementOwner(c) if err != nil { 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") } 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) err := q.Where("request_id = ?", req.RequestID).First(&key).Error if err == nil { + if key.DeletedAt != nil { + return buserr.New("ErrAPIKeyNotFound") + } if key.RequestHash != requestHash { 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 { - owner, err := s.owner(c) + owner, err := s.managementOwner(c) if err != nil { 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 { - owner, err := s.owner(c) + owner, err := s.managementOwner(c) if err != nil { 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 { - owner, err := s.owner(c) + owner, err := s.managementOwner(c) if err != nil { 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 { if req.ID == "" || req.Revision == 0 { return buserr.New("ErrInvalidParams") } apiKeyMutationMu.Lock() 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 { - 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) { return buserr.New("ErrAPIKeyNotFound") } return err } - if key.Revision != req.Revision || key.Status == "Revoked" { + if key.Revision != req.Revision || (key.Status == "Revoked" && action != "delete") { return buserr.New("ErrAPIKeyConflict") } before := key diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index 1df02a9c7..6a4976157 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -17,6 +17,10 @@ const docTemplate = `{ }, "host": "", "basePath": "/api/v2", + "schemes": [ + "http", + "https" + ], "paths": { "/ai/accounts": { "post": { @@ -74,7 +78,6 @@ const docTemplate = `{ "description": "OK", "schema": { "additionalProperties": { - "format": "int64", "type": "integer" }, "type": "object" @@ -3907,11 +3910,11 @@ const docTemplate = `{ "Alert" ], "x-panel-log": { + "BeforeFunctions": [], "bodyKeys": [], - "paramKeys": [], - "formatZH": "删除告警配置 [name]", "formatEN": "delete alert config [name]", - "BeforeFunctions": [] + "formatZH": "删除告警配置 [name]", + "paramKeys": [] } } }, @@ -3971,6 +3974,50 @@ const docTemplate = `{ ] } }, + "/alert/config/status": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AlertConfigStatusUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update alert config status", + "tags": [ + "Alert" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "status" + ], + "formatEN": "update alert config status [name][status]", + "formatZH": "更新告警配置状态 [name][status]", + "paramKeys": [] + } + } + }, "/alert/config/test": { "post": { "consumes": [ @@ -4040,13 +4087,13 @@ const docTemplate = `{ "Alert" ], "x-panel-log": { + "BeforeFunctions": [], "bodyKeys": [ "displayName" ], - "paramKeys": [], - "formatZH": "更新告警配置 [displayName]", "formatEN": "update alert config [displayName]", - "BeforeFunctions": [] + "formatZH": "更新告警配置 [displayName]", + "paramKeys": [] } } }, @@ -8033,6 +8080,14 @@ const docTemplate = `{ }, "/containers/list/stats": { "get": { + "parameters": [ + { + "description": "Comma-separated container IDs; omitted selects all containers", + "in": "query", + "name": "ids", + "type": "string" + } + ], "responses": { "200": { "description": "OK", @@ -8792,6 +8847,14 @@ const docTemplate = `{ "consumes": [ "application/json" ], + "parameters": [ + { + "description": "Only count containers", + "in": "query", + "name": "containersOnly", + "type": "boolean" + } + ], "produces": [ "application/json" ], @@ -9377,23 +9440,43 @@ const docTemplate = `{ "consumes": [ "application/json" ], + "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.", + "produces": [ + "application/json" + ], "responses": { "200": { - "description": "OK", + "description": "code=200 with the new secret; authentication and business errors use envelope code 400, 401 or 500", + "headers": { + "Cache-Control": { + "description": "no-store", + "type": "string" + } + }, "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "properties": { + "data": { + "type": "string" + } + }, + "type": "object" + } + ] } } }, "security": [ { - "ApiKeyAuth": [] - }, - { - "Timestamp": [] + "CSRFToken": [], + "PanelSession": [] } ], - "summary": "generate api key", + "summary": "Generate or reset the legacy API key", "tags": [ "Auth" ], @@ -9406,11 +9489,562 @@ const docTemplate = `{ } } }, + "/core/auth/api/keys/create": { + "post": { + "consumes": [ + "application/json" + ], + "description": "Accepts a panel login session with CSRF protection, or a valid API Key signature. The new key belongs to the authenticated account.\nrequestID is optional. Omitted or empty values are generated by the server. For idempotent retries, supply a UUID and reuse it with the same payload.\nA 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.\nOmitted expiresAt defaults to 90 days; explicit null means no expiration. Omitted apiKeyValidityTime defaults to 120 minutes; zero disables timestamp validation.\nAPI-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.\nAuthentication 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.", + "parameters": [ + { + "description": "Key policy and an optional UUID requestID for idempotency", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.APIKeyCreate" + } + }, + { + "description": "Optional ID of the authenticating independent key; omit for legacy keys", + "in": "header", + "name": "1Panel-Key-ID", + "type": "string" + }, + { + "description": "Signature algorithm; omission accepts either supported algorithm", + "enum": [ + "v1", + "md5", + "hmac-sha256" + ], + "in": "header", + "name": "1Panel-Signature-Version", + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "code=200 on success; data is null on errors", + "headers": { + "Cache-Control": { + "description": "no-store", + "type": "string" + } + }, + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "properties": { + "data": { + "$ref": "#/definitions/dto.APIKeyCreated" + } + }, + "type": "object" + } + ] + } + }, + "403": { + "description": "Invalid or missing CSRF token for a session request", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + }, + "security": [ + { + "ApiKeyAuth": [], + "Timestamp": [] + }, + { + "CSRFToken": [], + "PanelSession": [] + } + ], + "summary": "Create an API key for the current account", + "tags": [ + "API Key" + ] + } + }, + "/core/auth/api/keys/delete": { + "post": { + "consumes": [ + "application/json" + ], + "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.\nSubmit 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.\nA key can delete itself; subsequent requests signed with it fail. Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500.", + "parameters": [ + { + "description": "Independent key ID and current revision", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.APIKeyMutation" + } + }, + { + "description": "Optional ID of the authenticating independent key; omit for legacy keys", + "in": "header", + "name": "1Panel-Key-ID", + "type": "string" + }, + { + "description": "Signature algorithm; omission accepts either supported algorithm", + "enum": [ + "v1", + "md5", + "hmac-sha256" + ], + "in": "header", + "name": "1Panel-Signature-Version", + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "code=200 on success; data is null on errors", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "properties": { + "data": { + "$ref": "#/definitions/dto.APIKeyMutationResult" + } + }, + "type": "object" + } + ] + } + }, + "403": { + "description": "Invalid or missing CSRF token for a session request", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + }, + "security": [ + { + "ApiKeyAuth": [], + "Timestamp": [] + }, + { + "CSRFToken": [], + "PanelSession": [] + } + ], + "summary": "Delete an independent API key", + "tags": [ + "API Key" + ] + } + }, + "/core/auth/api/keys/reveal": { + "post": { + "consumes": [ + "application/json" + ], + "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.\nOnly 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.\nSecret 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.", + "parameters": [ + { + "description": "Independent key ID and current revision", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.APIKeyMutation" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "code=200 on success; data is null on errors", + "headers": { + "Cache-Control": { + "description": "no-store", + "type": "string" + } + }, + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "properties": { + "data": { + "$ref": "#/definitions/dto.APIKeyRevealed" + } + }, + "type": "object" + } + ] + } + }, + "403": { + "description": "Invalid or missing CSRF token", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + }, + "security": [ + { + "CSRFToken": [], + "PanelSession": [] + } + ], + "summary": "Reveal an independent API key's plaintext secret", + "tags": [ + "API Key" + ] + } + }, + "/core/auth/api/keys/revoke": { + "post": { + "consumes": [ + "application/json" + ], + "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.\nSubmit 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.\nA 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.\nAuthentication and business errors use HTTP 200 with envelope code 400, 401 or 500.", + "parameters": [ + { + "description": "Key ID and current revision", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.APIKeyMutation" + } + }, + { + "description": "Optional ID of the authenticating independent key; omit for legacy keys", + "in": "header", + "name": "1Panel-Key-ID", + "type": "string" + }, + { + "description": "Signature algorithm; omission accepts either supported algorithm", + "enum": [ + "v1", + "md5", + "hmac-sha256" + ], + "in": "header", + "name": "1Panel-Signature-Version", + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "code=200 on success; data is null on errors", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "properties": { + "data": { + "$ref": "#/definitions/dto.APIKeyMutationResult" + } + }, + "type": "object" + } + ] + } + }, + "403": { + "description": "Invalid or missing CSRF token for a session request", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + }, + "security": [ + { + "ApiKeyAuth": [], + "Timestamp": [] + }, + { + "CSRFToken": [], + "PanelSession": [] + } + ], + "summary": "Revoke an API key", + "tags": [ + "API Key" + ] + } + }, + "/core/auth/api/keys/search": { + "post": { + "consumes": [ + "application/json" + ], + "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.\nReturns key metadata and masked hints, never plaintext secrets. The legacy key is included when present; deleted keys are excluded.\nAuthentication and business errors use HTTP 200 with envelope code 400, 401 or 500; check code rather than HTTP status alone.", + "parameters": [ + { + "description": "Pagination and revoked-key filter", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.APIKeySearch" + } + }, + { + "description": "Optional ID of the authenticating independent key; omit for legacy keys", + "in": "header", + "name": "1Panel-Key-ID", + "type": "string" + }, + { + "description": "Signature algorithm; omission accepts either supported algorithm", + "enum": [ + "v1", + "md5", + "hmac-sha256" + ], + "in": "header", + "name": "1Panel-Signature-Version", + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "code=200 on success; data is null on errors", + "headers": { + "Cache-Control": { + "description": "no-store", + "type": "string" + } + }, + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "properties": { + "data": { + "$ref": "#/definitions/dto.APIKeyPage" + } + }, + "type": "object" + } + ] + } + }, + "403": { + "description": "Invalid or missing CSRF token for a session request", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + }, + "security": [ + { + "ApiKeyAuth": [], + "Timestamp": [] + }, + { + "CSRFToken": [], + "PanelSession": [] + } + ], + "summary": "List the current account's API keys", + "tags": [ + "API Key" + ] + } + }, + "/core/auth/api/keys/status": { + "post": { + "consumes": [ + "application/json" + ], + "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.\nSubmit 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.\nSuccess increments the revision; search again before another mutation. Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500.", + "parameters": [ + { + "description": "Key ID, current revision and Enable or Disable status", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.APIKeyStatus" + } + }, + { + "description": "Optional ID of the authenticating independent key; omit for legacy keys", + "in": "header", + "name": "1Panel-Key-ID", + "type": "string" + }, + { + "description": "Signature algorithm; omission accepts either supported algorithm", + "enum": [ + "v1", + "md5", + "hmac-sha256" + ], + "in": "header", + "name": "1Panel-Signature-Version", + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "code=200 on success; data is null on errors", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "properties": { + "data": { + "$ref": "#/definitions/dto.APIKeyMutationResult" + } + }, + "type": "object" + } + ] + } + }, + "403": { + "description": "Invalid or missing CSRF token for a session request", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + }, + "security": [ + { + "ApiKeyAuth": [], + "Timestamp": [] + }, + { + "CSRFToken": [], + "PanelSession": [] + } + ], + "summary": "Enable or disable an API key", + "tags": [ + "API Key" + ] + } + }, + "/core/auth/api/keys/update": { + "post": { + "consumes": [ + "application/json" + ], + "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.\nSubmit 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.\nUse id=legacy for the legacy key; its name is fixed and expiresAt must be null. Revoked or deleted independent keys cannot be updated.\nSuccess increments the revision; search again before another mutation. Authentication and business errors use HTTP 200 with envelope code 400, 401 or 500.", + "parameters": [ + { + "description": "Key ID, current revision and complete editable policy", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.APIKeyUpdate" + } + }, + { + "description": "Optional ID of the authenticating independent key; omit for legacy keys", + "in": "header", + "name": "1Panel-Key-ID", + "type": "string" + }, + { + "description": "Signature algorithm; omission accepts either supported algorithm", + "enum": [ + "v1", + "md5", + "hmac-sha256" + ], + "in": "header", + "name": "1Panel-Signature-Version", + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "code=200 on success; data is null on errors", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/dto.Response" + }, + { + "properties": { + "data": { + "$ref": "#/definitions/dto.APIKeyMutationResult" + } + }, + "type": "object" + } + ] + } + }, + "403": { + "description": "Invalid or missing CSRF token for a session request", + "schema": { + "$ref": "#/definitions/dto.Response" + } + } + }, + "security": [ + { + "ApiKeyAuth": [], + "Timestamp": [] + }, + { + "CSRFToken": [], + "PanelSession": [] + } + ], + "summary": "Update an API key's policy", + "tags": [ + "API Key" + ] + } + }, "/core/auth/api/update": { "post": { "consumes": [ "application/json" ], + "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.", "parameters": [ { "description": "request", @@ -9422,20 +10056,24 @@ const docTemplate = `{ } } ], + "produces": [ + "application/json" + ], "responses": { "200": { - "description": "OK" + "description": "code=200 on success; authentication and business errors use envelope code 400, 401 or 500", + "schema": { + "$ref": "#/definitions/dto.Response" + } } }, "security": [ { - "ApiKeyAuth": [] - }, - { - "Timestamp": [] + "CSRFToken": [], + "PanelSession": [] } ], - "summary": "Update api config", + "summary": "Update the legacy API key configuration", "tags": [ "Auth" ], @@ -11286,6 +11924,47 @@ const docTemplate = `{ } } }, + "/core/settings/file/download": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.FileDownloadPreference" + } + } + }, + "summary": "Load current user's file download preference", + "tags": [ + "System Setting" + ] + }, + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.FileDownloadPreference" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Update current user's file download preference", + "tags": [ + "System Setting" + ] + } + }, "/core/settings/interface": { "get": { "consumes": [ @@ -12999,6 +13678,136 @@ const docTemplate = `{ } } }, + "/dashboard/quick/directory": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/model.QuickDirectory" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List quick directories", + "tags": [ + "Dashboard" + ] + }, + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.QuickDirectoryCreate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Create quick directory", + "tags": [ + "Dashboard" + ] + } + }, + "/dashboard/quick/directory/del": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.QuickDirectoryDelete" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Delete quick directory", + "tags": [ + "Dashboard" + ] + } + }, + "/dashboard/quick/directory/update": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.QuickDirectoryUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Update quick directory", + "tags": [ + "Dashboard" + ] + } + }, "/dashboard/quick/option": { "get": { "responses": { @@ -17971,6 +18780,53 @@ const docTemplate = `{ ] } }, + "/files/wget/process/remove": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileProcessRemoveReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.FileProcessKeys" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Remove finished download progress records without deleting files", + "tags": [ + "File" + ], + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "keys" + ], + "formatEN": "Remove finished download records [keys]", + "formatZH": "移除已结束下载记录 [keys]", + "paramKeys": [] + } + } + }, "/files/wget/stop": { "post": { "consumes": [ @@ -19818,6 +20674,17 @@ const docTemplate = `{ }, "/hosts/monitor/clean": { "post": { + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MonitorClean" + } + } + ], "responses": { "200": { "description": "OK" @@ -19831,15 +20698,17 @@ const docTemplate = `{ "Timestamp": [] } ], - "summary": "Clean monitor data", + "summary": "Clean host or GPU monitor data", "tags": [ "Monitor" ], "x-panel-log": { "BeforeFunctions": [], - "bodyKeys": [], - "formatEN": "clean monitor datas", - "formatZH": "清空监控数据", + "bodyKeys": [ + "type" + ], + "formatEN": "clean [type] monitoring data", + "formatZH": "清空 [type] 监控数据", "paramKeys": [] } } @@ -20559,6 +21428,18 @@ const docTemplate = `{ "in": "query", "name": "command", "type": "string" + }, + { + "description": "session id to reattach", + "in": "query", + "name": "session", + "type": "string" + }, + { + "description": "allow recovery after an unexpected disconnect", + "in": "query", + "name": "terminalPersistent", + "type": "boolean" } ], "responses": { @@ -20580,6 +21461,88 @@ const docTemplate = `{ ] } }, + "/hosts/terminal/sessions/close": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TerminalSessionClose" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Close a terminal session", + "tags": [ + "Terminal" + ] + } + }, + "/hosts/terminal/sessions/closeAll": { + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "tags": [ + "Terminal" + ] + } + }, + "/hosts/terminal/sessions/search": { + "post": { + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/terminal.Info" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List the caller's live terminal sessions", + "tags": [ + "Terminal" + ] + } + }, "/hosts/terminal/ssh": { "get": { "parameters": [ @@ -20594,6 +21557,24 @@ const docTemplate = `{ "in": "query", "name": "command", "type": "string" + }, + { + "description": "session id to reattach", + "in": "query", + "name": "session", + "type": "string" + }, + { + "description": "session title shown in the session list", + "in": "query", + "name": "title", + "type": "string" + }, + { + "description": "allow recovery after an unexpected disconnect", + "in": "query", + "name": "terminalPersistent", + "type": "boolean" } ], "responses": { @@ -22181,12 +23162,21 @@ const docTemplate = `{ "Runtime" ], "x-panel-log": { - "BeforeFunctions": [], - "bodyKeys": [ - "id" + "BeforeFunctions": [ + { + "db": "runtimes", + "input_column": "id", + "input_value": "ID", + "isList": false, + "output_column": "name", + "output_value": "name" + } ], - "formatEN": "Operate runtime [id]", - "formatZH": "操作运行环境 [id]", + "bodyKeys": [ + "ID" + ], + "formatEN": "Operate runtime [name]", + "formatZH": "操作运行环境 [name]", "paramKeys": [] } } @@ -26307,6 +27297,41 @@ const docTemplate = `{ } } }, + "/websites/ca/import": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/request.WebsiteCAImport" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Import website CA from PEM or server files", + "tags": [ + "Website CA" + ] + } + }, "/websites/ca/obtain": { "post": { "consumes": [ @@ -26451,6 +27476,39 @@ const docTemplate = `{ ] } }, + "/websites/ca/upload": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "parameters": [ + { + "description": "ZIP containing ca.crt and ca.key", + "in": "formData", + "name": "file", + "required": true, + "type": "file" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Upload website CA archive", + "tags": [ + "Website CA" + ] + } + }, "/websites/ca/{id}": { "get": { "consumes": [ @@ -30152,32 +31210,283 @@ const docTemplate = `{ } }, "definitions": { - "dto.AgentFeishuTransition": { - "type": "object", + "dto.APIKeyCreate": { "properties": { - "confirmed": { + "allowAppBinding": { + "default": false, "type": "boolean" }, - "streaming": { - "type": "boolean" + "apiKeyValidityTime": { + "example": 120, + "maximum": 1440, + "minimum": 0, + "type": "integer" }, - "groupSessionScope": { - "type": "string", - "enum": [ - "group", - "group_sender", - "group_topic", - "group_topic_sender" - ] + "apiTrustedProxies": { + "example": "192.0.2.1", + "maxLength": 4096, + "type": "string" }, - "requireMention": { + "description": { + "example": "Used by the deployment service", + "maxLength": 256, + "type": "string" + }, + "expiresAt": { + "example": "2030-01-01T00:00:00Z", + "format": "date-time", "type": "string", - "enum": [ - "true", - "false" - ] + "x-nullable": true + }, + "ipWhiteList": { + "example": "192.0.2.10", + "maxLength": 4096, + "type": "string" + }, + "name": { + "example": "Deployment automation", + "maxLength": 64, + "minLength": 1, + "type": "string" + }, + "requestID": { + "description": "Optional client-generated UUID for idempotent retries. Omitted or empty values are generated by the server.", + "example": "df1a75a3-5f56-4fa3-9e7a-a573a853d894", + "format": "uuid", + "type": "string" } - } + }, + "type": "object" + }, + "dto.APIKeyCreated": { + "properties": { + "alreadyCreated": { + "type": "boolean" + }, + "apiKey": { + "type": "string" + }, + "item": { + "$ref": "#/definitions/dto.APIKeyItem" + } + }, + "type": "object" + }, + "dto.APIKeyItem": { + "properties": { + "allowAppBinding": { + "example": false, + "type": "boolean" + }, + "apiKeyValidityTime": { + "example": 120, + "type": "integer" + }, + "apiTrustedProxies": { + "example": "192.0.2.1", + "type": "string" + }, + "createdAt": { + "format": "date-time", + "type": "string", + "x-nullable": true + }, + "description": { + "example": "Used by the deployment service", + "type": "string" + }, + "expiresAt": { + "format": "date-time", + "type": "string", + "x-nullable": true + }, + "id": { + "example": "8a8f0f77-1ec8-4d46-8f5a-0ea8807e2d8a", + "type": "string" + }, + "ipWhiteList": { + "example": "192.0.2.10", + "type": "string" + }, + "keyHint": { + "example": "abcd••••1234", + "type": "string" + }, + "kind": { + "enum": [ + "apiKey", + "legacy" + ], + "example": "apiKey", + "type": "string" + }, + "name": { + "example": "Deployment automation", + "type": "string" + }, + "revision": { + "example": 1, + "minimum": 1, + "type": "integer" + }, + "status": { + "enum": [ + "Enable", + "Disable", + "Revoked", + "Expired" + ], + "example": "Enable", + "type": "string" + } + }, + "type": "object" + }, + "dto.APIKeyMutation": { + "properties": { + "id": { + "example": "8a8f0f77-1ec8-4d46-8f5a-0ea8807e2d8a", + "type": "string" + }, + "revision": { + "example": 1, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "dto.APIKeyMutationResult": { + "properties": { + "terminalClosePending": { + "type": "boolean" + } + }, + "type": "object" + }, + "dto.APIKeyPage": { + "properties": { + "items": { + "items": { + "$ref": "#/definitions/dto.APIKeyItem" + }, + "type": "array" + }, + "total": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.APIKeyRevealed": { + "properties": { + "apiKey": { + "type": "string" + }, + "id": { + "example": "8a8f0f77-1ec8-4d46-8f5a-0ea8807e2d8a", + "type": "string" + }, + "revision": { + "example": 1, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "dto.APIKeySearch": { + "properties": { + "excludeRevoked": { + "default": false, + "type": "boolean" + }, + "page": { + "example": 1, + "minimum": 1, + "type": "integer" + }, + "pageSize": { + "example": 20, + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "dto.APIKeyStatus": { + "properties": { + "id": { + "example": "8a8f0f77-1ec8-4d46-8f5a-0ea8807e2d8a", + "type": "string" + }, + "revision": { + "example": 1, + "minimum": 1, + "type": "integer" + }, + "status": { + "enum": [ + "Enable", + "Disable" + ], + "example": "Disable", + "type": "string" + } + }, + "type": "object" + }, + "dto.APIKeyUpdate": { + "properties": { + "allowAppBinding": { + "default": false, + "type": "boolean" + }, + "apiKeyValidityTime": { + "example": 120, + "maximum": 1440, + "minimum": 0, + "type": "integer" + }, + "apiTrustedProxies": { + "example": "192.0.2.1", + "maxLength": 4096, + "type": "string" + }, + "description": { + "example": "Used by the deployment service", + "maxLength": 256, + "type": "string" + }, + "expiresAt": { + "example": "2030-01-01T00:00:00Z", + "format": "date-time", + "type": "string", + "x-nullable": true + }, + "id": { + "example": "8a8f0f77-1ec8-4d46-8f5a-0ea8807e2d8a", + "type": "string" + }, + "ipWhiteList": { + "example": "192.0.2.10", + "maxLength": 4096, + "type": "string" + }, + "name": { + "example": "Deployment automation", + "maxLength": 64, + "minLength": 1, + "type": "string" + }, + "revision": { + "example": 1, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" }, "dto.AgentAccountCreateReq": { "properties": { @@ -30245,6 +31554,9 @@ const docTemplate = `{ "apiType": { "type": "string" }, + "appInstallId": { + "type": "integer" + }, "authMode": { "type": "string" }, @@ -30883,6 +32195,9 @@ const docTemplate = `{ }, "dto.AgentCreateReq": { "properties": { + "accessHost": { + "type": "string" + }, "accountId": { "type": "integer" }, @@ -30893,7 +32208,9 @@ const docTemplate = `{ "enum": [ "openclaw", "copaw", - "hermes-agent" + "hermes-agent", + "deepseek-harness", + "octop" ], "type": "string" }, @@ -31277,9 +32594,6 @@ const docTemplate = `{ }, "dto.AgentFeishuConfig": { "properties": { - "builtinManaged": { - "type": "boolean" - }, "bots": { "items": { "$ref": "#/definitions/dto.AgentFeishuBot" @@ -31289,6 +32603,9 @@ const docTemplate = `{ "builtinAvailable": { "type": "boolean" }, + "builtinManaged": { + "type": "boolean" + }, "connectionMode": { "type": "string" }, @@ -31308,13 +32625,7 @@ const docTemplate = `{ "type": "string" }, "groupSessionScope": { - "type": "string", - "enum": [ - "group", - "group_sender", - "group_topic", - "group_topic_sender" - ] + "type": "string" }, "installed": { "type": "boolean" @@ -31322,6 +32633,9 @@ const docTemplate = `{ "legacy": { "type": "boolean" }, + "legacyConfigured": { + "type": "boolean" + }, "replyMode": { "type": "string" }, @@ -31333,49 +32647,10 @@ const docTemplate = `{ }, "threadSession": { "type": "boolean" - }, - "legacyConfigured": { - "type": "boolean" } }, "type": "object" }, - "dto.AgentFeishuSwitchReq": { - "properties": { - "agentId": { - "type": "integer" - }, - "groupSessionScope": { - "type": "string", - "enum": [ - "group", - "group_sender", - "group_topic", - "group_topic_sender" - ] - }, - "requireMention": { - "type": "string", - "enum": [ - "true", - "false" - ] - }, - "streaming": { - "type": "boolean" - }, - "taskID": { - "type": "string" - } - }, - "required": [ - "agentId", - "taskID", - "groupSessionScope", - "requireMention" - ], - "type": "object" - }, "dto.AgentFeishuConfigReq": { "properties": { "agentId": { @@ -31423,13 +32698,13 @@ const docTemplate = `{ "type": "string" }, "groupSessionScope": { - "type": "string", "enum": [ "group", "group_sender", "group_topic", "group_topic_sender" - ] + ], + "type": "string" }, "replyMode": { "type": "string" @@ -31458,6 +32733,59 @@ const docTemplate = `{ ], "type": "object" }, + "dto.AgentFeishuSwitchReq": { + "properties": { + "agentId": { + "type": "integer" + }, + "groupSessionScope": { + "enum": [ + "group", + "group_sender", + "group_topic", + "group_topic_sender" + ], + "type": "string" + }, + "requireMention": { + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "streaming": { + "type": "boolean" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "agentId", + "groupSessionScope", + "requireMention", + "taskID" + ], + "type": "object" + }, + "dto.AgentFeishuTransition": { + "properties": { + "confirmed": { + "type": "boolean" + }, + "groupSessionScope": { + "type": "string" + }, + "requireMention": { + "type": "string" + }, + "streaming": { + "type": "boolean" + } + }, + "type": "object" + }, "dto.AgentHermesChatSessionDeleteReq": { "properties": { "agentId": { @@ -31528,6 +32856,9 @@ const docTemplate = `{ }, "dto.AgentItem": { "properties": { + "accessHost": { + "type": "string" + }, "accountId": { "type": "integer" }, @@ -31629,6 +32960,12 @@ const docTemplate = `{ }, "type": "array" }, + "metadata": { + "items": { + "$ref": "#/definitions/dto.AgentModelMetadata" + }, + "type": "array" + }, "model": { "type": "string" } @@ -31649,6 +32986,12 @@ const docTemplate = `{ }, "type": "array" }, + "metadata": { + "items": { + "$ref": "#/definitions/dto.AgentModelMetadata" + }, + "type": "array" + }, "model": { "type": "string" } @@ -31660,6 +33003,34 @@ const docTemplate = `{ ], "type": "object" }, + "dto.AgentModelMetadata": { + "properties": { + "contextWindow": { + "minimum": 0, + "type": "integer" + }, + "inputMode": { + "enum": [ + "auto", + "text", + "image" + ], + "type": "string" + }, + "maxTokens": { + "minimum": 0, + "type": "integer" + }, + "model": { + "type": "string" + } + }, + "required": [ + "inputMode", + "model" + ], + "type": "object" + }, "dto.AgentOtherConfig": { "properties": { "browserEnabled": { @@ -32849,8 +34220,30 @@ const docTemplate = `{ ], "type": "object" }, + "dto.AlertConfigStatusUpdate": { + "properties": { + "id": { + "type": "integer" + }, + "status": { + "enum": [ + "Enable", + "Disable" + ], + "type": "string" + } + }, + "required": [ + "id", + "status" + ], + "type": "object" + }, "dto.AlertConfigTest": { "properties": { + "config": { + "type": "string" + }, "displayName": { "type": "string" }, @@ -32861,6 +34254,9 @@ const docTemplate = `{ "host": { "type": "string" }, + "id": { + "type": "integer" + }, "password": { "type": "string" }, @@ -32873,6 +34269,9 @@ const docTemplate = `{ "sender": { "type": "string" }, + "type": { + "type": "string" + }, "userName": { "type": "string" } @@ -32890,6 +34289,9 @@ const docTemplate = `{ "id": { "type": "integer" }, + "revision": { + "type": "string" + }, "status": { "type": "string" }, @@ -33293,14 +34695,14 @@ const docTemplate = `{ "dockerCompose": { "type": "string" }, - "version": { - "type": "string" + "feishuConfigured": { + "type": "boolean" }, "feishuLegacy": { "type": "boolean" }, - "feishuConfigured": { - "type": "boolean" + "version": { + "type": "string" } }, "type": "object" @@ -33555,11 +34957,14 @@ const docTemplate = `{ "properties": { "quicks": { "items": { - "$ref": "#/definitions/dto.QuickJump" + "$ref": "#/definitions/dto.QuickVisibility" }, "type": "array" } }, + "required": [ + "quicks" + ], "type": "object" }, "dto.ChangeRedisPass": { @@ -34398,6 +35803,9 @@ const docTemplate = `{ "containerID": { "type": "string" }, + "containerName": { + "type": "string" + }, "cpuPercent": { "type": "number" }, @@ -34863,6 +36271,14 @@ const docTemplate = `{ "alertTitle": { "type": "string" }, + "alertTriggerMode": { + "enum": [ + "failed", + "success", + "both" + ], + "type": "string" + }, "appID": { "type": "string" }, @@ -34992,6 +36408,14 @@ const docTemplate = `{ "alertTitle": { "type": "string" }, + "alertTriggerMode": { + "enum": [ + "failed", + "success", + "both" + ], + "type": "string" + }, "apps": { "items": { "$ref": "#/definitions/dto.TransHelper" @@ -35458,6 +36882,12 @@ const docTemplate = `{ "netBytesSent": { "type": "integer" }, + "npuData": { + "items": { + "$ref": "#/definitions/dto.NPUInfo" + }, + "type": "array" + }, "procs": { "type": "integer" }, @@ -35939,9 +37369,6 @@ const docTemplate = `{ }, "dto.DockerPortGuardBase": { "properties": { - "ipv6Enabled": { - "type": "boolean" - }, "backend": { "type": "string" }, @@ -35957,6 +37384,9 @@ const docTemplate = `{ "ipv6": { "$ref": "#/definitions/dto.DockerPortGuardFamilyStatus" }, + "ipv6Enabled": { + "type": "boolean" + }, "isExist": { "type": "boolean" }, @@ -36060,39 +37490,6 @@ const docTemplate = `{ }, "type": "object" }, - "dto.DockerPortGuardEndpointIdentity": { - "properties": { - "family": { - "enum": [ - "ipv4", - "ipv6" - ], - "type": "string" - }, - "hostIP": { - "maxLength": 45, - "type": "string" - }, - "hostPort": { - "minimum": 1, - "type": "integer" - }, - "protocol": { - "enum": [ - "tcp", - "udp" - ], - "type": "string" - } - }, - "required": [ - "family", - "hostIP", - "hostPort", - "protocol" - ], - "type": "object" - }, "dto.DockerPortGuardFamilyStatus": { "properties": { "bound": { @@ -36413,6 +37810,14 @@ const docTemplate = `{ ], "type": "object" }, + "dto.FileDownloadPreference": { + "properties": { + "useServerFilename": { + "type": "boolean" + } + }, + "type": "object" + }, "dto.FileManageAIInfo": { "properties": { "aiAccountId": { @@ -36481,15 +37886,15 @@ const docTemplate = `{ "initialized": { "type": "boolean" }, + "partial": { + "type": "boolean" + }, "raInterfaces": { "items": { "type": "string" }, "type": "array" }, - "partial": { - "type": "boolean" - }, "reason": { "type": "string" } @@ -36657,6 +38062,10 @@ const docTemplate = `{ }, "dto.FirewallLifecycleOperation": { "properties": { + "backupFile": { + "maxLength": 255, + "type": "string" + }, "operation": { "enum": [ "start", @@ -36946,22 +38355,6 @@ const docTemplate = `{ }, "type": "object" }, - "dto.FirewallRuleDeleteTarget": { - "properties": { - "instanceKey": { - "maxLength": 128, - "type": "string" - }, - "scope": { - "$ref": "#/definitions/filter.Scope" - } - }, - "required": [ - "instanceKey", - "scope" - ], - "type": "object" - }, "dto.FirewallRuleInventory": { "properties": { "actions": { @@ -37135,15 +38528,15 @@ const docTemplate = `{ }, "dto.FirewallSettings": { "properties": { - "ipv6Enabled": { - "type": "boolean" - }, "docker": { "$ref": "#/definitions/dto.FirewallBackendGroup" }, "forwarding": { "$ref": "#/definitions/dto.FirewallBackendGroup" }, + "ipv6Enabled": { + "type": "boolean" + }, "panelPort": { "type": "string" }, @@ -37167,9 +38560,6 @@ const docTemplate = `{ }, "dto.FirewallSubsystemStatus": { "properties": { - "ipv6Enabled": { - "type": "boolean" - }, "backend": { "type": "string" }, @@ -37182,6 +38572,9 @@ const docTemplate = `{ "ipv6": { "$ref": "#/definitions/dto.FirewallBackendFamilyStatus" }, + "ipv6Enabled": { + "type": "boolean" + }, "isActive": { "type": "boolean" }, @@ -37251,59 +38644,6 @@ const docTemplate = `{ }, "type": "object" }, - "dto.ForwardRule": { - "properties": { - "address": { - "type": "string" - }, - "chain": { - "type": "string" - }, - "description": { - "type": "string" - }, - "family": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "interface": { - "type": "string" - }, - "isDesired": { - "type": "boolean" - }, - "isRuntime": { - "type": "boolean" - }, - "num": { - "type": "string" - }, - "port": { - "type": "string" - }, - "protocol": { - "type": "string" - }, - "strategy": { - "type": "string" - }, - "syncStatus": { - "type": "string" - }, - "targetIP": { - "type": "string" - }, - "targetPort": { - "type": "string" - }, - "usedStatus": { - "type": "string" - } - }, - "type": "object" - }, "dto.ForwardRuleOperate": { "properties": { "import": { @@ -37477,6 +38817,12 @@ const docTemplate = `{ }, "dto.GPUInfo": { "properties": { + "busID": { + "type": "string" + }, + "chipIndex": { + "type": "integer" + }, "fanSpeed": { "type": "string" }, @@ -37498,6 +38844,9 @@ const docTemplate = `{ "memoryUsage": { "type": "string" }, + "npuIndex": { + "type": "integer" + }, "performanceState": { "type": "string" }, @@ -38097,6 +39446,9 @@ const docTemplate = `{ "needCaptcha": { "type": "boolean" }, + "panelCache": { + "type": "string" + }, "panelName": { "type": "string" }, @@ -38401,6 +39753,21 @@ const docTemplate = `{ ], "type": "object" }, + "dto.MonitorClean": { + "properties": { + "type": { + "enum": [ + "host", + "gpu" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, "dto.MonitorData": { "properties": { "date": { @@ -38421,12 +39788,81 @@ const docTemplate = `{ }, "dto.MonitorGPUData": { "properties": { + "aiCPUUtil": { + "items": { + "type": "number" + }, + "type": "array" + }, + "bucketSeconds": { + "type": "integer" + }, + "computeUtil": { + "items": { + "type": "number" + }, + "type": "array" + }, + "copyUtil": { + "items": { + "type": "number" + }, + "type": "array" + }, + "ctrlCPUUtil": { + "items": { + "type": "number" + }, + "type": "array" + }, "date": { "items": { "type": "string" }, "type": "array" }, + "ddrBandwidth": { + "items": { + "type": "number" + }, + "type": "array" + }, + "ddrTotal": { + "items": { + "type": "number" + }, + "type": "array" + }, + "ddrUsed": { + "items": { + "type": "number" + }, + "type": "array" + }, + "decoderUtil": { + "items": { + "type": "number" + }, + "type": "array" + }, + "encoderUtil": { + "items": { + "type": "number" + }, + "type": "array" + }, + "fanRPM": { + "items": { + "type": "number" + }, + "type": "array" + }, + "frequencyValue": { + "items": { + "type": "number" + }, + "type": "array" + }, "gpuProcesses": { "items": { "items": { @@ -38442,12 +39878,90 @@ const docTemplate = `{ }, "type": "array" }, + "hbmBandwidth": { + "items": { + "type": "number" + }, + "type": "array" + }, + "hbmTotal": { + "items": { + "type": "number" + }, + "type": "array" + }, + "hbmUsed": { + "items": { + "type": "number" + }, + "type": "array" + }, + "hotspotTemperature": { + "items": { + "type": "number" + }, + "type": "array" + }, + "hugepagesTotal": { + "items": { + "type": "number" + }, + "type": "array" + }, + "hugepagesUsed": { + "items": { + "type": "number" + }, + "type": "array" + }, + "jpegUtil": { + "items": { + "type": "number" + }, + "type": "array" + }, + "mediaFrequency": { + "items": { + "type": "number" + }, + "type": "array" + }, + "mediaUtil": { + "items": { + "type": "number" + }, + "type": "array" + }, + "memoryActivity": { + "items": { + "type": "number" + }, + "type": "array" + }, + "memoryBandwidth": { + "items": { + "type": "number" + }, + "type": "array" + }, + "memoryFrequencyValue": { + "items": { + "type": "number" + }, + "type": "array" + }, "memoryPercent": { "items": { "type": "number" }, "type": "array" }, + "memoryTemperatureValue": { + "items": { + "type": "number" + }, + "type": "array" + }, "memoryTotal": { "items": { "type": "number" @@ -38460,6 +39974,12 @@ const docTemplate = `{ }, "type": "array" }, + "ofaUtil": { + "items": { + "type": "number" + }, + "type": "array" + }, "powerPercent": { "items": { "type": "number" @@ -38480,13 +40000,16 @@ const docTemplate = `{ }, "processCount": { "items": { - "type": "integer" + "type": "number" }, "type": "array" }, + "sampleCount": { + "type": "integer" + }, "speedValue": { "items": { - "type": "integer" + "type": "number" }, "type": "array" }, @@ -38501,9 +40024,22 @@ const docTemplate = `{ }, "dto.MonitorGPUSearch": { "properties": { + "aggregation": { + "enum": [ + "avg", + "max" + ], + "type": "string" + }, + "deviceID": { + "type": "string" + }, "endTime": { "type": "string" }, + "legacy": { + "type": "boolean" + }, "productName": { "type": "string" }, @@ -38552,6 +40088,15 @@ const docTemplate = `{ "defaultNetwork": { "type": "string" }, + "gpuMonitorInterval": { + "type": "string" + }, + "gpuMonitorStatus": { + "type": "string" + }, + "gpuMonitorStoreDays": { + "type": "string" + }, "monitorInterval": { "type": "string" }, @@ -38560,6 +40105,15 @@ const docTemplate = `{ }, "monitorStoreDays": { "type": "string" + }, + "vllmMonitorInterval": { + "type": "string" + }, + "vllmMonitorStatus": { + "type": "string" + }, + "vllmMonitorStoreDays": { + "type": "string" } }, "type": "object" @@ -38571,6 +40125,12 @@ const docTemplate = `{ "MonitorStatus", "MonitorStoreDays", "MonitorInterval", + "GPUMonitorStatus", + "GPUMonitorStoreDays", + "GPUMonitorInterval", + "VLLMMonitorStatus", + "VLLMMonitorStoreDays", + "VLLMMonitorInterval", "DefaultNetwork", "DefaultIO" ], @@ -39176,6 +40736,65 @@ const docTemplate = `{ }, "type": "object" }, + "dto.NPUInfo": { + "properties": { + "aiCore": { + "type": "string" + }, + "busID": { + "type": "string" + }, + "chipIndex": { + "type": "integer" + }, + "hbmTotal": { + "type": "string" + }, + "hbmUsed": { + "type": "string" + }, + "health": { + "type": "string" + }, + "hugepagesTotal": { + "type": "string" + }, + "hugepagesUsed": { + "type": "string" + }, + "index": { + "type": "integer" + }, + "memTotal": { + "type": "string" + }, + "memUsed": { + "type": "string" + }, + "memoryTotal": { + "type": "string" + }, + "memoryUsed": { + "type": "string" + }, + "npuIndex": { + "type": "integer" + }, + "powerDraw": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "temperature": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, "dto.NetworkCreate": { "properties": { "auxAddress": { @@ -39257,7 +40876,8 @@ const docTemplate = `{ "ssl", "cache", "http-per", - "proxy-cache" + "proxy-cache", + "brotli" ], "type": "string", "x-enum-varnames": [ @@ -39266,7 +40886,8 @@ const docTemplate = `{ "SSL", "CACHE", "HttpPer", - "ProxyCache" + "ProxyCache", + "Brotli" ] }, "dto.NginxModuleArtifact": { @@ -40157,6 +41778,55 @@ const docTemplate = `{ }, "type": "object" }, + "dto.QuickDirectoryCreate": { + "properties": { + "alias": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "dto.QuickDirectoryDelete": { + "properties": { + "id": { + "type": "integer" + }, + "legacy": { + "type": "boolean" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "dto.QuickDirectoryUpdate": { + "properties": { + "alias": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "legacy": { + "type": "boolean" + }, + "path": { + "type": "string" + } + }, + "required": [ + "id", + "path" + ], + "type": "object" + }, "dto.QuickJump": { "properties": { "alias": { @@ -40186,6 +41856,20 @@ const docTemplate = `{ }, "type": "object" }, + "dto.QuickVisibility": { + "properties": { + "id": { + "type": "integer" + }, + "isShow": { + "type": "boolean" + } + }, + "required": [ + "id" + ], + "type": "object" + }, "dto.RecordFileSize": { "properties": { "id": { @@ -40245,12 +41929,12 @@ const docTemplate = `{ "type": "object" }, "dto.RedisCliInstall": { - "type": "object", "properties": { "taskID": { "type": "string" } - } + }, + "type": "object" }, "dto.RedisConf": { "properties": { @@ -40473,12 +42157,6 @@ const docTemplate = `{ "type": "string" }, "encryptionMode": { - "enum": [ - "rsa", - "ed25519", - "ecdsa", - "dsa" - ], "type": "string" }, "id": { @@ -40500,9 +42178,6 @@ const docTemplate = `{ "type": "string" } }, - "required": [ - "encryptionMode" - ], "type": "object" }, "dto.RunningTime": { @@ -41152,6 +42827,9 @@ const docTemplate = `{ "noAuthSetting": { "type": "string" }, + "panelCache": { + "type": "string" + }, "panelName": { "type": "string" }, @@ -41664,6 +43342,17 @@ const docTemplate = `{ }, "type": "object" }, + "dto.TerminalSessionClose": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, "dto.TerminalUpdate": { "properties": { "backgroundColor": { @@ -41697,6 +43386,10 @@ const docTemplate = `{ "type": "string" }, "showTerminalButton": { + "enum": [ + "Enable", + "Disable" + ], "type": "string" } }, @@ -41872,6 +43565,9 @@ const docTemplate = `{ "deviceName": { "type": "string" }, + "gpuUtil": { + "type": "string" + }, "memory": { "type": "string" }, @@ -41881,6 +43577,9 @@ const docTemplate = `{ "memoryUtil": { "type": "string" }, + "pciBdfAddress": { + "type": "string" + }, "power": { "type": "string" }, @@ -42250,36 +43949,6 @@ const docTemplate = `{ "ScopeNoticeFamilyUnavailable" ] }, - "github_com_1Panel-dev_1Panel_agent_utils_firewall_sync.ReasonCode": { - "enum": [ - "invalid_policy", - "already_exists_in_target", - "only_exists_in_target", - "read_only_rule" - ], - "type": "string", - "x-enum-varnames": [ - "ReasonInvalidPolicy", - "ReasonAlreadyExists", - "ReasonOnlyExistsInTarget", - "ReasonReadOnlyRule" - ] - }, - "github_com_1Panel-dev_1Panel_agent_utils_firewall_sync.Status": { - "enum": [ - "ready", - "existing", - "remove", - "blocked" - ], - "type": "string", - "x-enum-varnames": [ - "StatusReady", - "StatusExisting", - "StatusRemove", - "StatusBlocked" - ] - }, "mfa.Otp": { "properties": { "qrImage": { @@ -42444,6 +44113,12 @@ const docTemplate = `{ "message": { "type": "string" }, + "modelAccountBaseURL": { + "type": "string" + }, + "modelAccountBaseURLType": { + "type": "string" + }, "name": { "type": "string" }, @@ -42459,6 +44134,9 @@ const docTemplate = `{ "status": { "type": "string" }, + "syncModelAccount": { + "type": "boolean" + }, "updatedAt": { "type": "string" }, @@ -42500,6 +44178,29 @@ const docTemplate = `{ }, "type": "object" }, + "model.QuickDirectory": { + "properties": { + "alias": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "legacy": { + "type": "boolean" + }, + "path": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "type": "object" + }, "model.Runtime": { "properties": { "appDetailID": { @@ -42952,6 +44653,18 @@ const docTemplate = `{ "memoryUnit": { "type": "string" }, + "modelAccountBaseURL": { + "type": "string" + }, + "modelAccountBaseURLType": { + "enum": [ + "container", + "localhost", + "systemIP", + "custom" + ], + "type": "string" + }, "name": { "type": "string" }, @@ -42989,6 +44702,9 @@ const docTemplate = `{ "specifyIP": { "type": "string" }, + "syncModelAccount": { + "type": "boolean" + }, "taskID": { "type": "string" }, @@ -43073,6 +44789,9 @@ const docTemplate = `{ "favorite": { "type": "boolean" }, + "feishuTransition": { + "$ref": "#/definitions/dto.AgentFeishuTransition" + }, "forceDelete": { "type": "boolean" }, @@ -43087,9 +44806,6 @@ const docTemplate = `{ }, "taskID": { "type": "string" - }, - "feishuTransition": { - "$ref": "#/definitions/dto.AgentFeishuTransition" } }, "required": [ @@ -43175,6 +44891,18 @@ const docTemplate = `{ "memoryUnit": { "type": "string" }, + "modelAccountBaseURL": { + "type": "string" + }, + "modelAccountBaseURLType": { + "enum": [ + "container", + "localhost", + "systemIP", + "custom" + ], + "type": "string" + }, "params": { "additionalProperties": true, "type": "object" @@ -43194,6 +44922,9 @@ const docTemplate = `{ "specifyIP": { "type": "string" }, + "syncModelAccount": { + "type": "boolean" + }, "type": { "type": "string" }, @@ -44165,6 +45896,22 @@ const docTemplate = `{ ], "type": "object" }, + "request.FileProcessRemoveReq": { + "properties": { + "keys": { + "items": { + "type": "string" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "keys" + ], + "type": "object" + }, "request.FileProcessReq": { "properties": { "key": { @@ -44341,6 +46088,9 @@ const docTemplate = `{ }, "useProxy": { "type": "boolean" + }, + "useServerFilename": { + "type": "boolean" } }, "required": [ @@ -45678,6 +47428,9 @@ const docTemplate = `{ }, "request.RuntimeUpdate": { "properties": { + "appDetailId": { + "type": "integer" + }, "clean": { "type": "boolean" }, @@ -46144,6 +47897,38 @@ const docTemplate = `{ ], "type": "object" }, + "request.WebsiteCAImport": { + "properties": { + "certificate": { + "type": "string" + }, + "certificatePath": { + "type": "string" + }, + "name": { + "maxLength": 128, + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "privateKeyPath": { + "type": "string" + }, + "type": { + "enum": [ + "paste", + "local" + ], + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "type": "object" + }, "request.WebsiteCAObtain": { "properties": { "autoRenew": { @@ -47489,6 +49274,9 @@ const docTemplate = `{ "allowPort": { "type": "boolean" }, + "appKey": { + "type": "string" + }, "containerName": { "type": "string" }, @@ -47507,12 +49295,24 @@ const docTemplate = `{ "hostMode": { "type": "boolean" }, + "httpPort": { + "type": "integer" + }, "memoryLimit": { "type": "number" }, "memoryUnit": { "type": "string" }, + "modelAccountBaseURL": { + "type": "string" + }, + "modelAccountBaseURLType": { + "type": "string" + }, + "name": { + "type": "string" + }, "params": { "items": { "$ref": "#/definitions/response.AppParam" @@ -47537,6 +49337,9 @@ const docTemplate = `{ "specifyIP": { "type": "string" }, + "syncModelAccount": { + "type": "boolean" + }, "type": { "type": "string" }, @@ -47741,6 +49544,12 @@ const docTemplate = `{ "lastBackupAt": { "type": "string" }, + "modelAccountBaseURL": { + "type": "string" + }, + "modelAccountBaseURLType": { + "type": "string" + }, "name": { "type": "string" }, @@ -48357,6 +50166,17 @@ const docTemplate = `{ }, "type": "object" }, + "response.FileProcessKeys": { + "properties": { + "keys": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, "response.FileRemarksRes": { "properties": { "remarks": { @@ -49784,6 +51604,62 @@ const docTemplate = `{ } }, "type": "object" + }, + "terminal.Info": { + "properties": { + "attached": { + "type": "boolean" + }, + "createdAt": { + "type": "string" + }, + "detachedAt": { + "description": "zero while attached", + "type": "string" + }, + "hostId": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "persistent": { + "type": "boolean" + }, + "title": { + "type": "string" + } + }, + "type": "object" + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "description": "Signed API Key authentication. Send the signature, never the plaintext API Key, in 1Panel-Token.\nSupported formats: hex(HMAC-SHA256(API-Key, '1panel:' + UnixTimestamp)) or md5('1panel' + API-Key + UnixTimestamp).\nSend the matching Unix timestamp in 1Panel-Timestamp. Optionally set 1Panel-Signature-Version to hmac-sha256, v1 or md5; omission accepts either algorithm.\nIndependent keys may include their ID in 1Panel-Key-ID to select the credential; omit that header for a legacy key.\n` + "`" + `` + "`" + `` + "`" + `\neg:\ncurl -X GET \"http://{host}:{port}/api/v2/toolbox/device/base\" \\\n-H \"1Panel-Token: \u003c1panel_token\u003e\" \\\n-H \"1Panel-Timestamp: \u003ccurrent_unix_timestamp\u003e\"\n` + "`" + `` + "`" + `` + "`" + `\n- ` + "`" + `1Panel-Token` + "`" + ` is the key for the panel API Key.", + "in": "header", + "name": "1Panel-Token", + "type": "apiKey" + }, + "CSRFToken": { + "description": "For session-authenticated POST requests, copy the current pcsrftoken cookie value into X-CSRF-Token. API Key authentication does not require a CSRF token. Session-only endpoints reject 1Panel authentication headers even when a valid session cookie is present.", + "in": "header", + "name": "X-CSRF-Token", + "type": "apiKey" + }, + "PanelSession": { + "description": "Existing panel login session: psession=\u003csession-cookie\u003e. Swagger 2.0 represents the cookie as a header; browsers send the established cookie automatically and cannot set Cookie manually. Use a logged-in same-origin browser for session-only operations.", + "in": "header", + "name": "Cookie", + "type": "apiKey" + }, + "Timestamp": { + "description": "- ` + "`" + `1Panel-Timestamp` + "`" + ` is the Unix timestamp of the current time in seconds.", + "in": "header", + "name": "1Panel-Timestamp", + "type": "apiKey" } } }` diff --git a/core/cmd/server/main.go b/core/cmd/server/main.go index cd00c51ec..3c21328d3 100644 --- a/core/cmd/server/main.go +++ b/core/cmd/server/main.go @@ -18,7 +18,10 @@ import ( // @schemes http https // @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 eg: // @description curl -X GET "http://{host}:{port}/api/v2/toolbox/device/base" \ @@ -27,7 +30,7 @@ import ( // @description ``` // @description - `1Panel-Token` is the key for the panel API Key. // @type apiKey -// @in Header +// @in header // @name 1Panel-Token // @securityDefinitions.apikey Timestamp // @type apiKey diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index be2117e05..379f7464f 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -72,5 +72,6 @@ func coreMigrations() []*gormigrate.Migration { migrations.UpdateAiMenuOrder, migrations.UpdateAiMenuOrderV2, migrations.AddPanelMCP, + migrations.AddAPIKeyDeletedAt, } } diff --git a/core/init/migration/migrations/api_key.go b/core/init/migration/migrations/api_key.go index 911987c39..56d5fb4e1 100644 --- a/core/init/migration/migrations/api_key.go +++ b/core/init/migration/migrations/api_key.go @@ -15,3 +15,8 @@ var AddOperationLogAPIKey = &gormigrate.Migration{ ID: "20260915-add-operation-log-api-key", 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{}) }, +} diff --git a/core/middleware/operation.go b/core/middleware/operation.go index f2e73e69c..6a5dcc275 100644 --- a/core/middleware/operation.go +++ b/core/middleware/operation.go @@ -42,6 +42,10 @@ func OperationLog() gin.HandlerFunc { return } 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": c.Next() return diff --git a/core/router/ro_base.go b/core/router/ro_base.go index 0b5942512..e14fd5758 100644 --- a/core/router/ro_base.go +++ b/core/router/ro_base.go @@ -40,6 +40,8 @@ func (s *BaseRouter) InitRouter(Router *gin.RouterGroup) { authRouter.POST("/api/keys/update", baseApi.UpdateAPIKey) authRouter.POST("/api/keys/status", baseApi.SetAPIKeyStatus) 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.POST("/current/update", baseApi.UpdateCurrentUser) diff --git a/core/utils/apikey_audit/audit.go b/core/utils/apikey_audit/audit.go index 655a6c2bf..f9ac10d46 100644 --- a/core/utils/apikey_audit/audit.go +++ b/core/utils/apikey_audit/audit.go @@ -45,6 +45,8 @@ func Build(c *gin.Context, event Event) (*model.OperationLog, error) { "enable": {"启用 API Key", "Enable API key"}, "disable": {"停用 API Key", "Disable 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"}, } 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.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 != "" { record.IP = event.ClientIP diff --git a/frontend/src/api/interface/api-key.ts b/frontend/src/api/interface/api-key.ts index 38477bf87..11a57ba1e 100644 --- a/frontend/src/api/interface/api-key.ts +++ b/frontend/src/api/interface/api-key.ts @@ -30,4 +30,7 @@ export namespace APIKey { id: string; revision: number; } + export interface Revealed extends Reference { + apiKey: string; + } } diff --git a/frontend/src/api/modules/api-key.ts b/frontend/src/api/modules/api-key.ts index 04d91830f..9f7294558 100644 --- a/frontend/src/api/modules/api-key.ts +++ b/frontend/src/api/modules/api-key.ts @@ -6,7 +6,7 @@ const prefix = '/core/auth/api/keys'; export const searchAPIKeys = (params: { page: number; pageSize: number; excludeRevoked?: boolean }) => { return http.post(`${prefix}/search`, params); }; -export const createAPIKey = (params: APIKey.Editable & { requestID: string }) => { +export const createAPIKey = (params: APIKey.Editable & { requestID?: string }) => { return http.post(`${prefix}/create`, params); }; 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) => { 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(`${prefix}/reveal`, params); +}; diff --git a/frontend/src/components/api-key-management/editor.vue b/frontend/src/components/api-key-management/editor.vue index 010b246a7..d6f854146 100644 --- a/frontend/src/components/api-key-management/editor.vue +++ b/frontend/src/components/api-key-management/editor.vue @@ -6,27 +6,14 @@ :confirm-before-close="true" @before-close="beforeClose" > - - +
  • @@ -139,13 +126,10 @@ @@ -154,7 +138,7 @@