http_client = $http_client; $this->config = $config; $this->app_auth_url = $this->buildAppAuthUrl(); $this->user_auth_url = $this->buildUserAuthUrl(); } public static function create(?ContaiConfig $config = null): self { if ($config === null) { $config = ContaiConfig::getInstance(); } $http_client = ContaiHTTPClientService::create($config); return new self($http_client, $config); } private function buildAppAuthUrl(): string { $base_url = rtrim($this->config->getApiBaseUrl(), '/'); return $base_url . $this->config->getAuthEndpoint(); } private function buildUserAuthUrl(): string { $base_url = rtrim($this->config->getApiBaseUrl(), '/'); return $base_url . $this->config->getUserTokenEndpoint(); } /** * Build authentication headers for all API requests. * * Returns both Authorization (app token) and x-user-token (user token). * Auto-refreshes expired tokens transparently. * * cURL example for a typical authenticated API request (import to Postman): * curl -X GET https://api-qa.1platform.pro/api/v1/users/profile \ * -H "Content-Type: application/json" \ * -H "Authorization: Bearer " \ * -H "x-user-token: " * * @return array|null Headers array or null if token acquisition failed. */ public function getAuthHeaders(): ?array { $app_token = $this->getAppToken(); if ($app_token === null) { $this->storeError(self::OPTION_APP_TOKEN_ERROR, 'Failed to obtain app authentication token'); return null; } $this->clearError(self::OPTION_APP_TOKEN_ERROR); $user_token = $this->getUserToken($app_token); if ($user_token === null) { $this->storeError(self::OPTION_USER_TOKEN_ERROR, 'Failed to obtain user authentication token. Check your API key.'); return null; } $this->clearError(self::OPTION_USER_TOKEN_ERROR); return [ 'Authorization' => 'Bearer ' . $app_token, 'x-user-token' => $user_token, ]; } /** * Build auth headers with app token only (no user token). * * Used by flows where the user doesn't exist yet (e.g. onboarding). */ public function getAppOnlyAuthHeaders(): ?array { $app_token = $this->getAppToken(); if ($app_token === null) { $this->storeError(self::OPTION_APP_TOKEN_ERROR, 'Failed to obtain app authentication token'); return null; } $this->clearError(self::OPTION_APP_TOKEN_ERROR); return [ 'Authorization' => 'Bearer ' . $app_token, ]; } /** * @deprecated Use getAuthHeaders() instead. Returns app token for backward compatibility. */ public function getToken(): ?string { return $this->getAppToken(); } /** * Get a valid app token, refreshing if expired. * * cURL example (import to Postman): * curl -X POST https://api-qa.1platform.pro/api/v1/auth/token \ * -H "Content-Type: application/json" \ * -d '{"apiKey": ""}' */ private function getAppToken(): ?string { $cached = $this->getCachedOptionToken(self::OPTION_APP_TOKEN, self::OPTION_APP_TOKEN_EXPIRES); if ($cached !== null) { return $cached; } return $this->generateNewAppToken(); } private function generateNewAppToken(): ?string { $api_key = $this->config->getApiKey(); $masked_key = !empty($api_key) ? substr($api_key, 0, 8) . '...' : '(empty)'; contai_log(sprintf('Content AI App Auth: requesting token from %s (key: %s)', $this->app_auth_url, $masked_key)); $response = $this->http_client->post($this->app_auth_url, [ self::API_KEY_FIELD => $api_key, ]); return $this->processTokenResponse( $response, self::OPTION_APP_TOKEN, self::OPTION_APP_TOKEN_EXPIRES, self::ERROR_APP_AUTH_HTTP, self::ERROR_APP_AUTH_FAILED, self::ERROR_APP_NO_TOKEN ); } /** * Get a valid user token, refreshing if expired. * * cURL example (import to Postman): * curl -X POST https://api-qa.1platform.pro/api/v1/users/token \ * -H "Content-Type: application/json" \ * -H "Authorization: Bearer " \ * -d '{"apiKey": ""}' */ private function getUserToken(string $app_token): ?string { $cached = $this->getCachedOptionToken(self::OPTION_USER_TOKEN, self::OPTION_USER_TOKEN_EXPIRES); if ($cached !== null) { return $cached; } return $this->generateNewUserToken($app_token); } private function generateNewUserToken(string $app_token): ?string { $user_api_key = $this->config->getUserApiKey(); if (empty($user_api_key)) { contai_log('Content AI User Auth: No user API key configured'); return null; } $masked_key = substr($user_api_key, 0, 8) . '...'; contai_log(sprintf('Content AI User Auth: requesting token from %s (key: %s)', $this->user_auth_url, $masked_key)); $response = $this->http_client->post($this->user_auth_url, [ self::API_KEY_FIELD => $user_api_key, ], [ 'Authorization' => 'Bearer ' . $app_token, ]); return $this->processTokenResponse( $response, self::OPTION_USER_TOKEN, self::OPTION_USER_TOKEN_EXPIRES, self::ERROR_USER_AUTH_HTTP, self::ERROR_USER_AUTH_FAILED, self::ERROR_USER_NO_TOKEN ); } private function processTokenResponse( ContaiHTTPResponse $response, string $token_option, string $expires_option, string $error_http_template, string $error_failed_template, string $error_no_token_message ): ?string { if (!$response->isSuccess()) { $error = $response->getError() ?: 'HTTP ' . $response->getStatusCode(); contai_log(sprintf($error_http_template, $error)); return null; } $data = $response->getJson(); if (!isset($data['success']) || !$data['success']) { $message = $data['msg'] ?? 'Unknown error'; contai_log(sprintf($error_failed_template, $message)); return null; } $token = $data['data'][self::RESPONSE_ACCESS_TOKEN_KEY] ?? null; $expires_in = $data['data'][self::RESPONSE_EXPIRES_IN_KEY] ?? self::DEFAULT_TOKEN_EXPIRY; if (!$token) { contai_log($error_no_token_message); return null; } $this->storeOptionToken($token, $expires_in, $token_option, $expires_option); return $token; } private function getCachedOptionToken(string $token_option, string $expires_option): ?string { $token = get_option($token_option, ''); if (empty($token)) { return null; } $expires_at = (int) get_option($expires_option, 0); if (empty($expires_at)) { return null; } $buffer_time = $this->config->getTokenBufferTime(); if (time() >= ($expires_at - $buffer_time)) { return null; } return $token; } private function storeOptionToken(string $token, int $expires_in, string $token_option, string $expires_option): void { $expires_at = time() + $expires_in; update_option($token_option, $token, false); update_option($expires_option, $expires_at, false); } /** * Force-refresh both app and user tokens regardless of expiration. * * Clears stored tokens, generates fresh ones, and returns the new * auth headers. Used by the admin "Refresh Tokens" button. * * @return array{success: bool, message: string, headers?: array} */ public function forceRefreshAllTokens(): array { $this->clearToken(); $this->clearErrors(); $app_token = $this->generateNewAppToken(); if ($app_token === null) { $error = self::getAppTokenError(); return [ 'success' => false, 'message' => $error ?? 'Failed to refresh app token', ]; } $user_token = $this->generateNewUserToken($app_token); if ($user_token === null) { $error = self::getUserTokenError(); return [ 'success' => false, 'message' => $error ?? 'Failed to refresh user token. Check your API key.', ]; } $this->clearError(self::OPTION_APP_TOKEN_ERROR); $this->clearError(self::OPTION_USER_TOKEN_ERROR); return [ 'success' => true, 'message' => 'Tokens refreshed successfully', ]; } public function clearToken(): void { delete_transient(self::LEGACY_TRANSIENT_TOKEN); delete_transient(self::LEGACY_TRANSIENT_EXPIRES); delete_option(self::OPTION_APP_TOKEN); delete_option(self::OPTION_APP_TOKEN_EXPIRES); delete_option(self::OPTION_USER_TOKEN); delete_option(self::OPTION_USER_TOKEN_EXPIRES); delete_option(self::OPTION_APP_TOKEN_ERROR); delete_option(self::OPTION_USER_TOKEN_ERROR); } public function clearUserToken(): void { delete_option(self::OPTION_USER_TOKEN); delete_option(self::OPTION_USER_TOKEN_EXPIRES); delete_option(self::OPTION_USER_TOKEN_ERROR); } public function getTokenInfo(): array { $app_token = get_option(self::OPTION_APP_TOKEN, ''); $app_expires = (int) get_option(self::OPTION_APP_TOKEN_EXPIRES, 0); $user_token = get_option(self::OPTION_USER_TOKEN, ''); $user_expires = (int) get_option(self::OPTION_USER_TOKEN_EXPIRES, 0); $now = time(); $buffer = $this->config->getTokenBufferTime(); return [ 'app_token' => [ 'has_token' => !empty($app_token), 'expires_at' => $app_expires > 0 ? $app_expires : null, 'is_expired' => empty($app_token) || $now >= ($app_expires - $buffer), 'time_until_expiry' => $app_expires > 0 ? max(0, $app_expires - $now) : 0, ], 'user_token' => [ 'has_token' => !empty($user_token), 'expires_at' => $user_expires > 0 ? $user_expires : null, 'is_expired' => empty($user_token) || $now >= ($user_expires - $buffer), 'time_until_expiry' => $user_expires > 0 ? max(0, $user_expires - $now) : 0, ], 'has_token' => !empty($app_token) && !empty($user_token), 'is_expired' => empty($app_token) || $now >= ($app_expires - $buffer) || empty($user_token) || $now >= ($user_expires - $buffer), ]; } public function validateToken(): bool { $app_token = get_option(self::OPTION_APP_TOKEN, ''); $user_token = get_option(self::OPTION_USER_TOKEN, ''); if (empty($app_token) || empty($user_token)) { return false; } $buffer = $this->config->getTokenBufferTime(); $now = time(); $app_expires = (int) get_option(self::OPTION_APP_TOKEN_EXPIRES, 0); $user_expires = (int) get_option(self::OPTION_USER_TOKEN_EXPIRES, 0); return $now < ($app_expires - $buffer) && $now < ($user_expires - $buffer); } public function clearErrors(): void { $this->clearError(self::OPTION_APP_TOKEN_ERROR); $this->clearError(self::OPTION_USER_TOKEN_ERROR); } private function storeError(string $option_key, string $message): void { update_option($option_key, $message, false); } private function clearError(string $option_key): void { delete_option($option_key); } public static function getAppTokenError(): ?string { $error = get_option(self::OPTION_APP_TOKEN_ERROR, ''); return !empty($error) ? $error : null; } public static function getUserTokenError(): ?string { $error = get_option(self::OPTION_USER_TOKEN_ERROR, ''); return !empty($error) ? $error : null; } public static function hasTokenErrors(): bool { return self::getAppTokenError() !== null || self::getUserTokenError() !== null; } }