package v2 import ( "encoding/json" "errors" "io" "net/http" "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" "github.com/1Panel-dev/1Panel/core/app/auth" "github.com/1Panel-dev/1Panel/core/app/dto" "github.com/1Panel-dev/1Panel/core/app/service" "github.com/1Panel-dev/1Panel/core/buserr" "github.com/gin-gonic/gin" ) func bindAPIKeyRequest(c *gin.Context, value interface{}) bool { if _, err := auth.RequireAPIKeyManagement(c); err != nil { helper.BadAuth(c, "ErrNotLogin", nil) return false } decoder := json.NewDecoder(http.MaxBytesReader(c.Writer, c.Request.Body, 32<<10)) decoder.DisallowUnknownFields() if err := decoder.Decode(value); err != nil { helper.BadRequest(c, err) return false } if err := decoder.Decode(new(interface{})); err != io.EOF { helper.BadRequest(c, errors.New("unexpected JSON data")) return false } return true } func apiKeyError(c *gin.Context, err error) { var business buserr.BusinessError if errors.As(err, &business) { helper.ErrorWithDetail(c, http.StatusBadRequest, business.Msg, business.Err) return } 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) { return } result, err := service.NewAPIKeyService().Search(c, req) if err != nil { apiKeyError(c, err) return } c.Header("Cache-Control", "no-store") 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) { return } result, err := service.NewAPIKeyService().Create(c, req) if err != nil { apiKeyError(c, err) return } c.Header("Cache-Control", "no-store") 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) { return } if err := service.NewAPIKeyService().Update(c, req); err != nil { apiKeyError(c, err) return } 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) { return } if err := service.NewAPIKeyService().Status(c, req); err != nil { apiKeyError(c, err) return } 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) { return } if err := service.NewAPIKeyService().Revoke(c, req); err != nil { apiKeyError(c, err) return } apiKeyMutationSuccess(c) } func apiKeyMutationSuccess(c *gin.Context) { 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) }