diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b735373 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..066b2d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a0453ac --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,58 @@ +# NOT READY FOR REVIEW +- (Edit the above to reflect status) + +# Summary +- TL;DR - what's this PR for? + +# Review By (Date) +- When does this need to be reviewed by? + +# Criticality +- How critical is this PR on a 1-10 scale? Also see [Severity Assessment](https://stanfordits.atlassian.net/browse/D8CORE-1705). +- E.g., it affects one site, or every site and product? + +# Review Tasks + +## Setup tasks and/or behavior to test + +1. Check out this branch +2. Navigate to... +3. Verify... + +## Front End Validation +- [ ] Is the markup using the appropriate semantic tags and passes HTML validation? +- [ ] Cross-browser testing has been performed? +- [ ] Automated accessibility scans performed? +- [ ] Manual accessibility tests performed? +- [ ] Design is approved by @ user? + +## Backend / Functional Validation +### Code +- [ ] Are the naming conventions following our standards? +- [ ] Does the code have sufficient inline comments? +- [ ] Is there anything in this code that would be hidden or hard to discover through the UI? +- [ ] Are there any [code smells](https://blog.codinghorror.com/code-smells/)? +- [ ] Are tests provided? eg (unit, behat, or codeception) + +### Code security +- [ ] Are all [forms properly sanitized](https://www.drupal.org/docs/8/security/drupal-8-sanitizing-output)? +- [ ] Any obvious [security flaws or new areas for attack](https://www.drupal.org/docs/8/security)? + +## General +- [ ] Is there anything included in this PR that is not related to the problem it is trying to solve? +- [ ] Is the approach to the problem appropriate? + +# Affected Projects or Products +- Does this PR impact any particular projects, products, or modules? + +# Associated Issues and/or People +- JIRA ticket(s) +- Other PRs +- Any other contextual information that might be helpful (e.g., description of a bug that this PR fixes, new functionality that it adds, etc.) +- Anyone who should be notified? (`@mention` them here) + +# Resources +- [SiteImprove](https://siteimprove.stanford.edu) +- [Accessibility Manual Test Script](https://docs.google.com/document/d/1ZXJ9RIUNXsS674ow9j3qJ2g1OAkCjmqMXl0Gs8XHEPQ/edit?usp=sharing) +- [HTML Validator](https://validator.w3.org/) +- [Browserstack](https://live.browserstack.com/dashboard) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dc42231 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +# .github/workflows/release.yml +name: Release + +on: + pull_request: + types: closed + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Tag + uses: K-Phoen/semver-release-action@master + with: + release_branch: master + tag_format: "%major%.%minor%.%patch%" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 3d036a8..bf6ff5a 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,14 @@ -# churro +# CHURRO Cloud Hosting Usage Reporting with Recurring Output ## Usage -1. Add your Acquia API key and secret to a file called `.env.local` at the root of the repository directory: +1. Add your Acquia API key, API secret, subscription UUID, and your Views/Visits entitlements to a file called `.env.local` at the root of the repository directory: ``` ACQUIA_API_KEY= ACQUIA_API_SECRET= NEXT_PUBLIC_ACQUIA_SUBSCRIPTION_UUID= NEXT_PUBLIC_ACQUIA_MONTHLY_VIEWS_ENTITLEMENT= NEXT_PUBLIC_ACQUIA_MONTHLY_VISITS_ENTITLEMENT= - ``` (no quotes) diff --git a/app/api/acquia/applications/route.ts b/app/api/acquia/applications/route.ts index 3a230e4..808b927 100644 --- a/app/api/acquia/applications/route.ts +++ b/app/api/acquia/applications/route.ts @@ -1,8 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; -import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed'; +import AcquiaApiServiceFixed from '@/lib/acquia-api'; export async function GET(request: NextRequest) { - console.log('๐Ÿš€ Applications API Route called'); + // console.log('๐Ÿš€ Applications API Route called'); // Update the API service initialization with better error handling if (!process.env.ACQUIA_API_KEY || !process.env.ACQUIA_API_SECRET) { @@ -31,11 +31,11 @@ export async function GET(request: NextRequest) { apiSecret: process.env.ACQUIA_API_SECRET!, }); - console.log('๐Ÿ”ง Using FIXED API Service for applications'); + // console.log('๐Ÿ”ง Using FIXED API Service for applications'); const applications = await apiService.getApplications(); - console.log('โœ… Successfully fetched applications data, count:', applications.length); + // console.log('โœ… Successfully fetched applications data, count:', applications.length); return NextResponse.json(applications); } catch (error) { diff --git a/app/api/acquia/views/route.ts b/app/api/acquia/views/route.ts index 43622c7..72cd719 100644 --- a/app/api/acquia/views/route.ts +++ b/app/api/acquia/views/route.ts @@ -1,17 +1,18 @@ import { NextRequest, NextResponse } from 'next/server'; -import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed'; +import AcquiaApiServiceFixed from '@/lib/acquia-api'; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const subscriptionUuid = searchParams.get('subscriptionUuid'); const from = searchParams.get('from'); const to = searchParams.get('to'); - + /** console.log('๐Ÿš€ Views by Application API Route called with params:', { subscriptionUuid, from, to, }); + */ if (!subscriptionUuid) { console.error('โŒ Missing required parameter: subscriptionUuid'); @@ -49,10 +50,10 @@ export async function GET(request: NextRequest) { }); apiService.setProgressCallback((progress) => { - console.log('๐Ÿ“ˆ Views progress:', progress); + // console.log('๐Ÿ“ˆ Views progress:', progress); }); - console.log('๐Ÿ”ง Using FIXED API Service for views by application (with pagination)'); + // console.log('๐Ÿ”ง Using FIXED API Service for views by application (with pagination)'); const data = await apiService.getViewsDataByApplication( subscriptionUuid, @@ -60,7 +61,7 @@ export async function GET(request: NextRequest) { to || undefined ); - console.log('โœ… Successfully fetched ALL views by application data, total count:', data.length); + // console.log('โœ… Successfully fetched ALL views by application data, total count:', data.length); return NextResponse.json({ data, diff --git a/app/api/acquia/visits/route.ts b/app/api/acquia/visits/route.ts index ce8d315..0e31426 100644 --- a/app/api/acquia/visits/route.ts +++ b/app/api/acquia/visits/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed'; +import AcquiaApiServiceFixed from '@/lib/acquia-api'; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; @@ -7,11 +7,13 @@ export async function GET(request: NextRequest) { const from = searchParams.get('from'); const to = searchParams.get('to'); + /** console.log('๐Ÿš€ Visits by Application API Route called with params:', { subscriptionUuid, from, to }); + */ if (!subscriptionUuid) { console.error('โŒ Missing required parameter: subscriptionUuid'); @@ -48,14 +50,14 @@ export async function GET(request: NextRequest) { }); apiService.setProgressCallback((progress) => { - console.log('๐Ÿ“Š Visits progress:', progress); + // console.log('๐Ÿ“Š Visits progress:', progress); }); - console.log('๐Ÿ”ง Using FIXED API Service for visits by application (with pagination)'); + // console.log('๐Ÿ”ง Using FIXED API Service for visits by application (with pagination)'); const data = await apiService.getVisitsDataByApplication(subscriptionUuid, from || undefined, to || undefined); - console.log('โœ… Successfully fetched ALL visits by application data, total count:', data.length); + // console.log('โœ… Successfully fetched ALL visits by application data, total count:', data.length); return NextResponse.json({ data, diff --git a/app/api/check-credentials/route.ts b/app/api/check-credentials/route.ts deleted file mode 100644 index eca7a86..0000000 --- a/app/api/check-credentials/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - const apiKey = process.env.ACQUIA_API_KEY; - const apiSecret = process.env.ACQUIA_API_SECRET; - - if (!apiKey || !apiSecret) { - return NextResponse.json({ error: 'Missing credentials' }); - } - - return NextResponse.json({ - api_key_analysis: { - value: apiKey, - length: apiKey.length, - format: { - is_uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(apiKey), - has_dashes: apiKey.includes('-'), - is_alphanumeric_only: /^[a-z0-9-]+$/i.test(apiKey) - } - }, - api_secret_analysis: { - length: apiSecret.length, - preview: apiSecret.substring(0, 10) + '...', - format: { - is_base64_like: /^[A-Za-z0-9+/]+=*$/.test(apiSecret), - ends_with_equals: apiSecret.endsWith('='), - has_special_chars: /[+/=]/.test(apiSecret) - } - }, - recommendations: [ - 'Verify these are Cloud API credentials (not Site Factory or other Acquia services)', - 'Check that the API application has the correct permissions', - 'Ensure the credentials are for the production Acquia Cloud API' - ] - }); -} \ No newline at end of file diff --git a/app/api/check-env-vars/route.ts b/app/api/check-env-vars/route.ts deleted file mode 100644 index 0f2a067..0000000 --- a/app/api/check-env-vars/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - // Get all environment variables that start with ACQUIA_ - const acquiaVars = Object.keys(process.env) - .filter(key => key.startsWith('ACQUIA_')) - .reduce((obj, key) => { - const value = process.env[key] || ''; - obj[key] = { - exists: !!value, - length: value.length, - preview: value.length > 20 ? value.substring(0, 20) + '...' : value, - full_value: key === 'ACQUIA_API_KEY' ? value : '[hidden]' // Only show full API key - }; - return obj; - }, {} as Record); - - return NextResponse.json({ - message: 'Environment Variables Check', - variables: acquiaVars, - expected_api_key: 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3', - api_key_matches: process.env.ACQUIA_API_KEY === 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3' - }); -} \ No newline at end of file diff --git a/app/api/check-env/route.ts b/app/api/check-env/route.ts deleted file mode 100644 index 9e63167..0000000 --- a/app/api/check-env/route.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import fs from 'fs'; -import path from 'path'; - -export async function GET(request: NextRequest) { - // Read the .env.local file directly to compare with process.env - let envFileContents = ''; - let envFilePath = ''; - let envFileError = null; - let envVarsFromFile: Record = {}; - - try { - envFilePath = path.join(process.cwd(), '.env.local'); - if (fs.existsSync(envFilePath)) { - envFileContents = fs.readFileSync(envFilePath, 'utf8'); - - // Parse .env.local file - envVarsFromFile = envFileContents - .split('\n') - .filter(line => line.trim() && !line.startsWith('#')) - .reduce((acc, line) => { - const match = line.match(/^([^=]+)=(.*)$/); - if (match) { - const key = match[1].trim(); - const value = match[2].trim(); - acc[key] = value; - } - return acc; - }, {} as Record); - } - } catch (error) { - envFileError = error instanceof Error ? error.message : 'Unknown error reading .env.local'; - } - - // Check for exact matches and transformations - const apiKeyMatches = process.env.ACQUIA_API_KEY === envVarsFromFile.ACQUIA_API_KEY; - const apiSecretMatches = process.env.ACQUIA_API_SECRET === envVarsFromFile.ACQUIA_API_SECRET; - - // Check if API key/secret are Base64 encoded versions - let apiKeyIsBase64OfEnvFile = false; - let apiSecretIsBase64OfEnvFile = false; - - try { - if (process.env.ACQUIA_API_KEY && envVarsFromFile.ACQUIA_API_KEY) { - const decodedApiKey = Buffer.from(process.env.ACQUIA_API_KEY, 'base64').toString('utf-8'); - apiKeyIsBase64OfEnvFile = decodedApiKey === envVarsFromFile.ACQUIA_API_KEY; - } - - if (process.env.ACQUIA_API_SECRET && envVarsFromFile.ACQUIA_API_SECRET) { - const decodedApiSecret = Buffer.from(process.env.ACQUIA_API_SECRET, 'base64').toString('utf-8'); - apiSecretIsBase64OfEnvFile = decodedApiSecret === envVarsFromFile.ACQUIA_API_SECRET; - } - } catch (error) { - // Not base64 encoded - } - - // Check if quoted values are being stripped - const apiKeyInFileHasQuotes = envVarsFromFile.ACQUIA_API_KEY?.startsWith('"') && - envVarsFromFile.ACQUIA_API_KEY?.endsWith('"'); - - const apiSecretInFileHasQuotes = envVarsFromFile.ACQUIA_API_SECRET?.startsWith('"') && - envVarsFromFile.ACQUIA_API_SECRET?.endsWith('"'); - - const apiKeyMatchesUnquoted = apiKeyInFileHasQuotes && - process.env.ACQUIA_API_KEY === envVarsFromFile.ACQUIA_API_KEY.slice(1, -1); - - const apiSecretMatchesUnquoted = apiSecretInFileHasQuotes && - process.env.ACQUIA_API_SECRET === envVarsFromFile.ACQUIA_API_SECRET.slice(1, -1); - - return NextResponse.json({ - env_file: { - path: envFilePath, - exists: !!envFileContents, - error: envFileError, - parsed_values: { - ACQUIA_API_KEY: envVarsFromFile.ACQUIA_API_KEY ? - `${envVarsFromFile.ACQUIA_API_KEY.substring(0, 8)}...` : 'not found', - ACQUIA_API_SECRET: envVarsFromFile.ACQUIA_API_SECRET ? - `${envVarsFromFile.ACQUIA_API_SECRET.substring(0, 8)}...` : 'not found', - ACQUIA_API_BASE_URL: envVarsFromFile.ACQUIA_API_BASE_URL, - ACQUIA_AUTH_BASE_URL: envVarsFromFile.ACQUIA_AUTH_BASE_URL, - has_quotes: { - ACQUIA_API_KEY: apiKeyInFileHasQuotes, - ACQUIA_API_SECRET: apiSecretInFileHasQuotes - } - } - }, - process_env: { - ACQUIA_API_KEY: process.env.ACQUIA_API_KEY ? - `${process.env.ACQUIA_API_KEY.substring(0, 8)}...` : undefined, - ACQUIA_API_SECRET: process.env.ACQUIA_API_SECRET ? - `${process.env.ACQUIA_API_SECRET.substring(0, 8)}...` : undefined, - ACQUIA_API_BASE_URL: process.env.ACQUIA_API_BASE_URL, - ACQUIA_AUTH_BASE_URL: process.env.ACQUIA_AUTH_BASE_URL, - NODE_ENV: process.env.NODE_ENV - }, - comparison: { - exact_match: { - ACQUIA_API_KEY: apiKeyMatches, - ACQUIA_API_SECRET: apiSecretMatches - }, - transformations: { - ACQUIA_API_KEY: { - is_base64_encoded: apiKeyIsBase64OfEnvFile, - quotes_removed: apiKeyMatchesUnquoted - }, - ACQUIA_API_SECRET: { - is_base64_encoded: apiSecretIsBase64OfEnvFile, - quotes_removed: apiSecretMatchesUnquoted - } - } - }, - solution: { - direct_env_usage: ` -// Solution 1: Access env variables directly (preferred) -const apiKey = process.env.ACQUIA_API_KEY; -const apiSecret = process.env.ACQUIA_API_SECRET; - `, - custom_env_loading: ` -// Solution 2: Custom env loading from file -import fs from 'fs'; -import path from 'path'; - -function loadEnvFromFile() { - try { - const envPath = path.join(process.cwd(), '.env.local'); - const envContent = fs.readFileSync(envPath, 'utf8'); - const envVars = {}; - - envContent.split('\\n').forEach(line => { - const parts = line.match(/^([^=]+)=(.*)$/); - if (parts) { - const key = parts[1].trim(); - let value = parts[2].trim(); - // Remove quotes if present - if (value.startsWith('"') && value.endsWith('"')) { - value = value.slice(1, -1); - } - envVars[key] = value; - } - }); - - return envVars; - } catch (e) { - console.error('Failed to load .env.local file'); - return {}; - } -} - -const envVars = loadEnvFromFile(); -const apiKey = envVars.ACQUIA_API_KEY; -const apiSecret = envVars.ACQUIA_API_SECRET; - ` - } - }); -} \ No newline at end of file diff --git a/app/api/debug-application/route.ts b/app/api/debug-application/route.ts deleted file mode 100644 index d884535..0000000 --- a/app/api/debug-application/route.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed'; - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const subscriptionUuid = searchParams.get('subscriptionUuid'); - const applicationUuid = searchParams.get('applicationUuid'); - const endpoint = searchParams.get('endpoint') || 'views'; // 'visits' or 'views' - - if (!subscriptionUuid) { - return NextResponse.json({ error: 'subscriptionUuid required' }, { status: 400 }); - } - - if (!process.env.ACQUIA_API_SECRET) { - return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); - } - - try { - const apiService = new AcquiaApiServiceFixed({ - baseUrl: process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api', - authUrl: process.env.ACQUIA_AUTH_BASE_URL || 'https://accounts.acquia.com/api', - apiKey: process.env.ACQUIA_API_KEY!, - apiSecret: process.env.ACQUIA_API_SECRET!, - }); - - // Get the access token - const token = await apiService.getAccessToken(); - const baseUrl = process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api'; - const fullUrl = `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/${endpoint}-by-application`; - - console.log('๐Ÿ” Making debug request to:', fullUrl); - - const response = await fetch(fullUrl, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': '*/*', - }, - }); - - const rawData = await response.json(); - - // Extract applications and their details - const applications: any[] = []; - - if (rawData._embedded?.metadata) { - rawData._embedded.metadata.forEach((metaItem: any, index: number) => { - if (metaItem.uuids && Array.isArray(metaItem.uuids)) { - metaItem.uuids.forEach((appUuid: string, appIndex: number) => { - const appInfo = { - uuid: appUuid, - name: metaItem.names?.[appIndex] || `App ${appUuid.substring(0, 8)}`, - metadataIndex: index, - environments: [], - totalDatapoints: 0, - sampleDatapoints: [] as any[] - }; - - // Add environment info if available - if (metaItem.ids && Array.isArray(metaItem.ids)) { - metaItem.ids.forEach((envId: string, envIndex: number) => { - appInfo.environments.push({ - id: envId, - name: metaItem.environmentNames?.[envIndex] || metaItem.environment_names?.[envIndex] || `Env ${envId.substring(0, 8)}` - }); - }); - } - - applications.push(appInfo); - }); - } - }); - } - - // Count datapoints and get samples - if (rawData._embedded?.datapoints) { - applications.forEach(app => { - app.totalDatapoints = rawData._embedded.datapoints.length; - app.sampleDatapoints = rawData._embedded.datapoints.slice(0, 5); - }); - } - - // Find the specific application if requested - let specificApplication = null; - if (applicationUuid) { - specificApplication = applications.find(app => app.uuid === applicationUuid); - } - - return NextResponse.json({ - url: fullUrl, - status: response.status, - totalApplications: applications.length, - totalDatapoints: rawData._embedded?.datapoints?.length || 0, - applications: applications, - specificApplication: specificApplication, - searchedFor: applicationUuid, - rawMetadata: rawData._embedded?.metadata || [], - sampleDatapoints: rawData._embedded?.datapoints?.slice(0, 10) || [] - }); - - } catch (error) { - return NextResponse.json({ - error: 'Debug request failed', - details: error instanceof Error ? error.message : String(error) - }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/debug-auth/route.ts b/app/api/debug-auth/route.ts deleted file mode 100644 index 5520fb0..0000000 --- a/app/api/debug-auth/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - // Get credentials from environment - const apiKey = process.env.ACQUIA_API_KEY; - const apiSecret = process.env.ACQUIA_API_SECRET; - const authUrl = process.env.ACQUIA_AUTH_BASE_URL || 'https://accounts.acquia.com/api'; - - // Check if credentials exist - if (!apiKey || !apiSecret) { - return NextResponse.json({ - error: 'Missing API credentials', - envVars: { - ACQUIA_API_KEY: apiKey ? `${apiKey.substring(0, 5)}...` : 'not set', - ACQUIA_API_SECRET: apiSecret ? `${apiSecret.substring(0, 5)}...` : 'not set', - ACQUIA_AUTH_BASE_URL: authUrl - } - }, { status: 400 }); - } - - try { - // Attempt authentication with debug info - const credentials = Buffer.from(`${apiKey}:${apiSecret}`).toString('base64'); - console.log('๐Ÿ“ Encoded credentials (first 10 chars):', credentials.substring(0, 10) + '...'); - - const formData = new URLSearchParams(); - formData.append('grant_type', 'client_credentials'); - - // Try authentication - const response = await fetch(`${authUrl}/auth/oauth/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': '*/*', - 'Authorization': `Basic ${credentials}` - }, - body: 'grant_type=client_credentials' - }); - - const data = await response.json(); - - if (response.ok) { - return NextResponse.json({ - success: true, - message: 'Authentication successful', - token_type: data.token_type, - expires_in: data.expires_in, - token_preview: data.access_token ? `${data.access_token.substring(0, 10)}...` : null - }); - } else { - return NextResponse.json({ - error: 'Authentication failed', - status: response.status, - data, - request_details: { - url: `${authUrl}/auth/oauth/token`, - api_key_preview: apiKey.substring(0, 5) + '...', - api_key_length: apiKey.length, - credentials_preview: credentials.substring(0, 10) + '...' - } - }, { status: response.status }); - } - } catch (error) { - return NextResponse.json({ - error: 'Failed to authenticate', - message: error instanceof Error ? error.message : String(error) - }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/debug-env-source/route.ts b/app/api/debug-env-source/route.ts deleted file mode 100644 index 9ddb8a1..0000000 --- a/app/api/debug-env-source/route.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import fs from 'fs'; -import path from 'path'; - -export async function GET(request: NextRequest) { - const projectRoot = process.cwd(); - - // Check for all possible env files - const envFiles = ['.env', '.env.local', '.env.development', '.env.production']; - const foundFiles: Record = {}; - - for (const file of envFiles) { - const filePath = path.join(projectRoot, file); - try { - if (fs.existsSync(filePath)) { - const content = fs.readFileSync(filePath, 'utf8'); - const lines = content.split('\n'); - const acquiaLines = lines.filter(line => - line.trim().startsWith('ACQUIA_API_KEY') || - line.trim().startsWith('ACQUIA_API_SECRET') - ); - - foundFiles[file] = { - exists: true, - acquiaLines: acquiaLines, - fullContent: content.substring(0, 500) + (content.length > 500 ? '...' : '') - }; - } else { - foundFiles[file] = { exists: false }; - } - } catch (error) { - foundFiles[file] = { - exists: false, - error: error instanceof Error ? error.message : String(error) - }; - } - } - - return NextResponse.json({ - message: 'Environment Files Debug', - project_root: projectRoot, - node_env: process.env.NODE_ENV, - current_values: { - ACQUIA_API_KEY: process.env.ACQUIA_API_KEY, - ACQUIA_API_SECRET: process.env.ACQUIA_API_SECRET ? '[hidden]' : undefined, - ACQUIA_API_KEY_LENGTH: process.env.ACQUIA_API_KEY?.length, - }, - env_files_found: foundFiles, - expected_api_key: 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3', - values_match: process.env.ACQUIA_API_KEY === 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3' - }); -} \ No newline at end of file diff --git a/app/api/debug-raw-response/route.ts b/app/api/debug-raw-response/route.ts deleted file mode 100644 index 6ea27a4..0000000 --- a/app/api/debug-raw-response/route.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed'; - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const subscriptionUuid = searchParams.get('subscriptionUuid'); - const endpoint = searchParams.get('endpoint') || 'visits'; // 'visits' or 'views' - - if (!subscriptionUuid) { - return NextResponse.json({ error: 'subscriptionUuid required' }, { status: 400 }); - } - - if (!process.env.ACQUIA_API_SECRET) { - return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); - } - - try { - const apiService = new AcquiaApiServiceFixed({ - baseUrl: process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api', - authUrl: process.env.ACQUIA_AUTH_BASE_URL || 'https://accounts.acquia.com/api', - apiKey: process.env.ACQUIA_API_KEY!, - apiSecret: process.env.ACQUIA_API_SECRET!, - }); - - // Get the access token using the public method - const token = await apiService.getAccessToken(); - const baseUrl = process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api'; - const fullUrl = `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/${endpoint}-by-application`; - - console.log('๐Ÿ” Making debug request to:', fullUrl); - - const response = await fetch(fullUrl, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': '*/*', - }, - }); - - const rawData = await response.json(); - - return NextResponse.json({ - url: fullUrl, - status: response.status, - headers: Object.fromEntries(response.headers.entries()), - rawResponse: rawData, - responseKeys: Object.keys(rawData), - embeddedKeys: rawData._embedded ? Object.keys(rawData._embedded) : null, - dataStructureAnalysis: analyzeDataStructure(rawData) - }); - - } catch (error) { - return NextResponse.json({ - error: 'Debug request failed', - details: error instanceof Error ? error.message : String(error) - }, { status: 500 }); - } -} - -function analyzeDataStructure(data: any, path = ''): any { - if (Array.isArray(data)) { - return { - type: 'array', - length: data.length, - firstItem: data.length > 0 ? analyzeDataStructure(data[0], `${path}[0]`) : null, - sampleItems: data.slice(0, 3).map((item, i) => analyzeDataStructure(item, `${path}[${i}]`)) - }; - } else if (data && typeof data === 'object') { - const keys = Object.keys(data); - const analysis: any = { - type: 'object', - keys: keys, - keyCount: keys.length - }; - - // Analyze first few keys - keys.slice(0, 5).forEach(key => { - analysis[key] = analyzeDataStructure(data[key], `${path}.${key}`); - }); - - return analysis; - } else { - return { - type: typeof data, - value: data - }; - } -} - -// Also export POST method if you want to test with POST requests -export async function POST(request: NextRequest) { - return NextResponse.json({ error: 'Use GET method' }, { status: 405 }); -} diff --git a/app/api/debug-raw-structure/route.ts b/app/api/debug-raw-structure/route.ts deleted file mode 100644 index 8e361bb..0000000 --- a/app/api/debug-raw-structure/route.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed'; - -// Add proper handling for circular references in JSON -function safeStringify(obj: any): string { - const seen = new WeakSet(); - return JSON.stringify(obj, (key, value) => { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return '[Circular Reference]'; - } - seen.add(value); - } - return value; - }, 2); -} - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const subscriptionUuid = searchParams.get('subscriptionUuid'); - const endpoint = searchParams.get('endpoint') || 'views'; - const from = searchParams.get('from'); - const to = searchParams.get('to'); - - if (!subscriptionUuid) { - return NextResponse.json({ error: 'subscriptionUuid required' }, { status: 400 }); - } - - if (!process.env.ACQUIA_API_SECRET) { - return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); - } - - try { - const apiService = new AcquiaApiServiceFixed({ - baseUrl: process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api', - authUrl: process.env.ACQUIA_AUTH_BASE_URL || 'https://accounts.acquia.com/api', - apiKey: process.env.ACQUIA_API_KEY!, - apiSecret: process.env.ACQUIA_API_SECRET!, - }); - - const token = await apiService.getAccessToken(); - const baseUrl = process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api'; - - // Build URL with date parameters if provided - const params = new URLSearchParams(); - if (from) params.append('from', from); - if (to) params.append('to', to); - - const fullUrl = `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/${endpoint}-by-application${params.toString() ? `?${params.toString()}` : ''}`; - - console.log('๐Ÿ” Making comprehensive debug request to:', fullUrl); - - const response = await fetch(fullUrl, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': '*/*', - }, - }); - - const rawData = await response.json(); - - // Comprehensive analysis function - function analyzeStructure(obj: any, path: string = '', maxDepth: number = 5): any { - if (maxDepth <= 0) return { type: typeof obj, note: 'max_depth_reached' }; - - if (obj === null) return { type: 'null' }; - if (obj === undefined) return { type: 'undefined' }; - - if (Array.isArray(obj)) { - return { - type: 'array', - length: obj.length, - isEmpty: obj.length === 0, - firstItems: obj.slice(0, 3).map((item, i) => ({ - index: i, - analysis: analyzeStructure(item, `${path}[${i}]`, maxDepth - 1) - })), - allItemTypes: [...new Set(obj.map(item => Array.isArray(item) ? 'array' : typeof item))] - }; - } - - if (typeof obj === 'object') { - const keys = Object.keys(obj); - const analysis: any = { - type: 'object', - keyCount: keys.length, - isEmpty: keys.length === 0, - keys: keys - }; - - // Analyze each key - keys.forEach(key => { - analysis[key] = analyzeStructure(obj[key], `${path}.${key}`, maxDepth - 1); - }); - - return analysis; - } - - return { - type: typeof obj, - value: typeof obj === 'string' ? obj.substring(0, 100) + (obj.length > 100 ? '...' : '') : obj - }; - } - - const structureAnalysis = analyzeStructure(rawData); - - // Look for any arrays or data structures that might contain our data - function findArraysAndData(obj: any, path: string = ''): any[] { - const results: any[] = []; - - if (Array.isArray(obj)) { - results.push({ - path, - type: 'array', - length: obj.length, - sample: obj.slice(0, 2) - }); - } else if (obj && typeof obj === 'object') { - Object.keys(obj).forEach(key => { - const fullPath = path ? `${path}.${key}` : key; - results.push(...findArraysAndData(obj[key], fullPath)); - }); - } - - return results; - } - - const foundArrays = findArraysAndData(rawData); - - // Check for any UUID-like strings anywhere in the response - function findUUIDs(obj: any, path: string = ''): any[] { - const results: any[] = []; - const uuidPattern = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi; - - if (typeof obj === 'string' && uuidPattern.test(obj)) { - results.push({ path, value: obj }); - } else if (Array.isArray(obj)) { - obj.forEach((item, index) => { - results.push(...findUUIDs(item, `${path}[${index}]`)); - }); - } else if (obj && typeof obj === 'object') { - Object.keys(obj).forEach(key => { - const fullPath = path ? `${path}.${key}` : key; - results.push(...findUUIDs(obj[key], fullPath)); - }); - } - - return results; - } - - const foundUUIDs = findUUIDs(rawData); - - // Check for any numeric data that might be metrics - function findNumericData(obj: any, path: string = ''): any[] { - const results: any[] = []; - - if (typeof obj === 'number') { - results.push({ path, value: obj }); - } else if (typeof obj === 'string' && !isNaN(Number(obj))) { - results.push({ path, value: obj, note: 'numeric_string' }); - } else if (Array.isArray(obj)) { - obj.forEach((item, index) => { - results.push(...findNumericData(item, `${path}[${index}]`)); - }); - } else if (obj && typeof obj === 'object') { - Object.keys(obj).forEach(key => { - const fullPath = path ? `${path}.${key}` : key; - results.push(...findNumericData(obj[key], fullPath)); - }); - } - - return results; - } - - const foundNumbers = findNumericData(rawData); - - const responseData = { - request: { - url: fullUrl, - method: 'GET', - hasDateRange: !!(from || to), - dateRange: { from, to } - }, - response: { - status: response.status, - statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()) - }, - analysis: { - topLevelKeys: Object.keys(rawData), - structureAnalysis, - foundArrays, - foundUUIDs, - foundNumbers: foundNumbers.slice(0, 20), // Limit to first 20 to avoid huge responses - totalNumericValues: foundNumbers.length - }, - rawResponse: rawData, - searchTargets: { - lookingFor: 'datapoints, metadata, applications with UUIDs', - targetUUID: '3e02ea73-76fa-4a88-91d7-3476aca3cf07' - } - }; - - return new NextResponse(safeStringify(responseData), { - status: 200, - headers: { - 'Content-Type': 'application/json' - } - }); - - } catch (error) { - const errorResponse = { - error: 'Debug request failed', - details: error instanceof Error ? error.message : String(error) - }; - - return new NextResponse(safeStringify(errorResponse), { - status: 500, - headers: { - 'Content-Type': 'application/json' - } - }); - } -} diff --git a/app/api/env-runtime-debug/route.ts b/app/api/env-runtime-debug/route.ts deleted file mode 100644 index 3613e3e..0000000 --- a/app/api/env-runtime-debug/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - // Capture the current state - const currentApiKey = process.env.ACQUIA_API_KEY; - - // Check if we can manually set it - const originalValue = process.env.ACQUIA_API_KEY; - process.env.ACQUIA_API_KEY = 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3'; - const afterManualSet = process.env.ACQUIA_API_KEY; - - // Restore original (for safety) - process.env.ACQUIA_API_KEY = originalValue; - - return NextResponse.json({ - message: 'Runtime Environment Debug', - original_value: originalValue, - after_manual_set: afterManualSet, - manual_set_worked: afterManualSet === 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3', - - // Check for any weird process.env behavior - process_env_keys: Object.keys(process.env).filter(k => k.includes('ACQUIA')), - - // Check if there are any descriptor issues - api_key_descriptor: Object.getOwnPropertyDescriptor(process.env, 'ACQUIA_API_KEY'), - - // Check the type - api_key_type: typeof process.env.ACQUIA_API_KEY, - api_key_constructor: process.env.ACQUIA_API_KEY?.constructor?.name, - }); -} \ No newline at end of file diff --git a/app/api/env-test/route.ts b/app/api/env-test/route.ts deleted file mode 100644 index a9eff5c..0000000 --- a/app/api/env-test/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - return NextResponse.json({ - node_env: process.env.NODE_ENV, - has_api_key: !!process.env.ACQUIA_API_KEY, - has_api_secret: !!process.env.ACQUIA_API_SECRET, - api_key_length: process.env.ACQUIA_API_KEY?.length || 0, - api_secret_length: process.env.ACQUIA_API_SECRET?.length || 0, - api_key_preview: process.env.ACQUIA_API_KEY?.substring(0, 8) + '...' || 'undefined', - all_env_keys: Object.keys(process.env).filter(key => key.startsWith('ACQUIA_')), - }); -} \ No newline at end of file diff --git a/app/api/generate-curl/route.ts b/app/api/generate-curl/route.ts deleted file mode 100644 index 9cfcacc..0000000 --- a/app/api/generate-curl/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - if (!process.env.ACQUIA_API_KEY || !process.env.ACQUIA_API_SECRET) { - return NextResponse.json( - { error: 'Missing API credentials in environment variables' }, - { status: 500 } - ); - } - - const apiKey = process.env.ACQUIA_API_KEY; - const apiSecret = process.env.ACQUIA_API_SECRET; - - // Generate curl commands for testing - const credentials = Buffer.from(`${apiKey}:${apiSecret}`).toString('base64'); - - const curlCommands = { - basic_auth_accounts: `curl -X POST "https://accounts.acquia.com/api/auth/oauth/token" \\ - -H "Content-Type: application/x-www-form-urlencoded" \\ - -H "Authorization: Basic ${credentials}" \\ - -H "Accept: application/json" \\ - -d "grant_type=client_credentials"`, - - form_params_accounts: `curl -X POST "https://accounts.acquia.com/api/auth/oauth/token" \\ - -H "Content-Type: application/x-www-form-urlencoded" \\ - -H "Accept: application/json" \\ - -d "grant_type=client_credentials&client_id=${apiKey}&client_secret=${apiSecret}"`, - - basic_auth_cloud: `curl -X POST "https://cloud.acquia.com/api/auth/oauth/token" \\ - -H "Content-Type: application/x-www-form-urlencoded" \\ - -H "Authorization: Basic ${credentials}" \\ - -H "Accept: application/json" \\ - -d "grant_type=client_credentials"`, - - form_params_cloud: `curl -X POST "https://cloud.acquia.com/api/auth/oauth/token" \\ - -H "Content-Type: application/x-www-form-urlencoded" \\ - -H "Accept: application/json" \\ - -d "grant_type=client_credentials&client_id=${apiKey}&client_secret=${apiSecret}"` - }; - - return NextResponse.json({ - message: 'Test these curl commands in your terminal to see which one works', - credentials_info: { - api_key: apiKey, - api_secret_length: apiSecret.length, - api_secret_preview: apiSecret.substring(0, 10) + '...', - base64_credentials_preview: credentials.substring(0, 20) + '...' - }, - curl_commands: curlCommands - }); -} \ No newline at end of file diff --git a/app/api/read-env-file/route.ts b/app/api/read-env-file/route.ts deleted file mode 100644 index b8a839e..0000000 --- a/app/api/read-env-file/route.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import fs from 'fs'; -import path from 'path'; - -export async function GET(request: NextRequest) { - try { - const envLocalPath = path.join(process.cwd(), '.env.local'); - - if (!fs.existsSync(envLocalPath)) { - return NextResponse.json({ - error: '.env.local file not found', - path: envLocalPath - }); - } - - const content = fs.readFileSync(envLocalPath, 'utf8'); - const lines = content.split('\n'); - - // Find the ACQUIA_API_KEY line - const apiKeyLine = lines.find(line => line.trim().startsWith('ACQUIA_API_KEY')); - const apiSecretLine = lines.find(line => line.trim().startsWith('ACQUIA_API_SECRET')); - - // Parse the values - let fileApiKey = ''; - let fileApiSecret = ''; - - if (apiKeyLine) { - const match = apiKeyLine.match(/ACQUIA_API_KEY\s*=\s*(.+)/); - fileApiKey = match ? match[1].trim() : ''; - } - - if (apiSecretLine) { - const match = apiSecretLine.match(/ACQUIA_API_SECRET\s*=\s*(.+)/); - fileApiSecret = match ? match[1].trim() : ''; - } - - return NextResponse.json({ - message: 'Direct .env.local file read', - file_path: envLocalPath, - file_exists: true, - api_key_line: apiKeyLine, - api_secret_line: apiSecretLine ? apiSecretLine.substring(0, 50) + '...' : undefined, - parsed_api_key: fileApiKey, - parsed_api_secret_preview: fileApiSecret ? fileApiSecret.substring(0, 20) + '...' : '', - process_env_api_key: process.env.ACQUIA_API_KEY, - values_match: fileApiKey === process.env.ACQUIA_API_KEY, - file_content_preview: content.substring(0, 300) + (content.length > 300 ? '...' : '') - }); - - } catch (error) { - return NextResponse.json({ - error: 'Failed to read .env.local file', - details: error instanceof Error ? error.message : String(error) - }); - } -} \ No newline at end of file diff --git a/app/api/test-auth/route.ts b/app/api/test-auth/route.ts deleted file mode 100644 index ec582aa..0000000 --- a/app/api/test-auth/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { testAcquiaAuth } from '@/lib/test-auth'; - -export async function GET(request: NextRequest) { - console.log('๐Ÿงช Test authentication endpoint called'); - - if (!process.env.ACQUIA_API_KEY || !process.env.ACQUIA_API_SECRET) { - return NextResponse.json( - { error: 'Missing API credentials in environment variables' }, - { status: 500 } - ); - } - - const result = await testAcquiaAuth( - process.env.ACQUIA_API_KEY, - process.env.ACQUIA_API_SECRET - ); - - return NextResponse.json(result); -} \ No newline at end of file diff --git a/app/api/test-date-format/route.ts b/app/api/test-date-format/route.ts deleted file mode 100644 index e940aa4..0000000 --- a/app/api/test-date-format/route.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed'; - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const subscriptionUuid = searchParams.get('subscriptionUuid'); - const from = searchParams.get('from'); - const to = searchParams.get('to'); - const endpoint = searchParams.get('endpoint') || 'views'; - - if (!subscriptionUuid) { - return NextResponse.json({ error: 'subscriptionUuid required' }, { status: 400 }); - } - - try { - const apiService = new AcquiaApiServiceFixed({ - baseUrl: process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api', - authUrl: process.env.ACQUIA_AUTH_BASE_URL || 'https://accounts.acquia.com/api', - apiKey: process.env.ACQUIA_API_KEY!, - apiSecret: process.env.ACQUIA_API_SECRET!, - }); - - const token = await apiService.getAccessToken(); - - // Test different URL formats - const baseUrl = `https://cloud.acquia.com/api/subscriptions/${subscriptionUuid}/metrics/usage/${endpoint}-by-application`; - - const testUrls = []; - - if (from && to) { - // Format 1: Our current format - const filter1 = `from=${from}T00:00:00.000Z,to=${to}T23:59:59.000Z`; - testUrls.push({ - name: 'Current format', - url: `${baseUrl}?filter=${encodeURIComponent(filter1)}&resolution=day`, - filter: filter1 - }); - - // Format 2: Your working example format - const filter2 = `from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z`; - testUrls.push({ - name: 'Working example format', - url: `${baseUrl}?filter=${encodeURIComponent(filter2)}&resolution=day`, - filter: filter2 - }); - } else { - // No date filter - testUrls.push({ - name: 'No date filter', - url: `${baseUrl}?resolution=day`, - filter: 'none' - }); - } - - const results = []; - - for (const testUrl of testUrls) { - try { - console.log(`Testing: ${testUrl.url}`); - - const response = await fetch(testUrl.url, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': '*/*', - }, - }); - - const data = await response.json(); - - const itemCount = data._embedded?.items?.length || 0; - let dateRange = 'no data'; - - if (itemCount > 0 && data._embedded.items[0].datapoints?.length > 0) { - const datapoints = data._embedded.items[0].datapoints; - const firstDate = Array.isArray(datapoints[0]) ? datapoints[0][0] : 'unknown'; - const lastDate = Array.isArray(datapoints[datapoints.length - 1]) ? datapoints[datapoints.length - 1][0] : 'unknown'; - dateRange = `${firstDate} to ${lastDate}`; - } - - results.push({ - ...testUrl, - status: response.status, - itemCount, - dateRange, - success: response.status === 200 - }); - } catch (error) { - results.push({ - ...testUrl, - status: 'error', - error: error instanceof Error ? error.message : String(error), - success: false - }); - } - } - - return NextResponse.json({ - message: 'Date format testing results', - requestedRange: { from, to }, - results - }); - - } catch (error) { - return NextResponse.json({ - error: 'Test failed', - details: error instanceof Error ? error.message : String(error) - }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/test-date-params/route.ts b/app/api/test-date-params/route.ts deleted file mode 100644 index 28320ff..0000000 --- a/app/api/test-date-params/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const from = searchParams.get('from'); - const to = searchParams.get('to'); - const subscriptionUuid = searchParams.get('subscriptionUuid'); - - // Test the exact URL format - const baseUrl = 'https://cloud.acquia.com/api'; - - // Build filter exactly as the API expects - let filterParts: string[] = []; - - if (from) { - const fromDate = from.includes('T') ? from : `${from}T00:00:00.000Z`; - filterParts.push(`from=${fromDate}`); - } - - if (to) { - const toDate = to.includes('T') ? to : `${to}T23:59:59.000Z`; - filterParts.push(`to=${toDate}`); - } - - const filterParam = filterParts.join(','); - - const testUrls = { - visits: `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/visits-by-application?filter=${encodeURIComponent(filterParam)}&resolution=day`, - views: `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/views-by-application?filter=${encodeURIComponent(filterParam)}&resolution=month`, - - // Compare with your working examples - yourExampleViews: `${baseUrl}/subscriptions/0bc0f7c5-b96a-43c9-b74f-3dd3810a5245/metrics/usage/views-by-application?filter=from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z&resolution=month`, - yourExampleVisits: `${baseUrl}/subscriptions/0bc0f7c5-b96a-43c9-b74f-3dd3810a5245/metrics/usage/visits-by-application?filter=from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z&resolution=day` - }; - - return NextResponse.json({ - input: { from, to, subscriptionUuid }, - filterParam, - encodedFilter: encodeURIComponent(filterParam), - testUrls, - comparison: { - ourFormat: filterParam, - expectedFormat: 'from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z', - matches: filterParam === 'from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z' - } - }); -} \ No newline at end of file diff --git a/app/api/test-fixed-auth/route.ts b/app/api/test-fixed-auth/route.ts deleted file mode 100644 index 07a5979..0000000 --- a/app/api/test-fixed-auth/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed'; - -export async function GET(request: NextRequest) { - console.log('๐Ÿงช Testing with FIXED API service...'); - - if (!process.env.ACQUIA_API_SECRET) { - return NextResponse.json({ error: 'Missing API secret' }, { status: 500 }); - } - - try { - const apiService = new AcquiaApiServiceFixed({ - baseUrl: process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api', - authUrl: process.env.ACQUIA_AUTH_BASE_URL || 'https://accounts.acquia.com/api', - apiKey: process.env.ACQUIA_API_KEY!, // This will be ignored and replaced with correct value - apiSecret: process.env.ACQUIA_API_SECRET!, - }); - - // Just test authentication - console.log('Testing authentication only...'); - - return NextResponse.json({ - success: true, - message: 'Fixed API service created successfully', - forced_api_key: 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3' - }); - - } catch (error) { - console.error('โŒ Fixed API service error:', error); - - return NextResponse.json({ - error: 'Failed with fixed API service', - details: error instanceof Error ? error.message : 'Unknown error' - }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/validate-credentials/route.ts b/app/api/validate-credentials/route.ts deleted file mode 100644 index b1a97fc..0000000 --- a/app/api/validate-credentials/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - const apiKey = process.env.ACQUIA_API_KEY; - const apiSecret = process.env.ACQUIA_API_SECRET; - - if (!apiKey || !apiSecret) { - return NextResponse.json({ - error: 'Missing credentials', - found_api_key: !!apiKey, - found_api_secret: !!apiSecret - }); - } - - // Detailed credential analysis - const analysis = { - api_key: { - value: apiKey, - length: apiKey.length, - has_whitespace: /\s/.test(apiKey), - is_uuid_format: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(apiKey), - char_codes: Array.from(apiKey).map(c => c.charCodeAt(0)), - }, - api_secret: { - value_preview: apiSecret.substring(0, 20) + '...', - length: apiSecret.length, - has_whitespace: /\s/.test(apiSecret), - is_base64_like: /^[A-Za-z0-9+/]+=*$/.test(apiSecret), - ends_with_equals: apiSecret.endsWith('='), - char_codes_sample: Array.from(apiSecret.substring(0, 20)).map(c => c.charCodeAt(0)), - } - }; - - return NextResponse.json({ - message: 'Credential Analysis', - analysis - }); -} \ No newline at end of file diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index 8773e75..96c1944 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -1,12 +1,11 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import { VisitsData, ViewsData, Application } from '@/lib/acquia-api-fixed'; +import { VisitsData, ViewsData, Application } from '@/lib/acquia-api'; import VisitsPieChart from './VisitsPieChart'; import ViewsPieChart from './ViewsPieChart'; -import SimpleVisitsBarChart from './SimpleVisitsBarChart'; // Use this -import SimpleViewsBarChart from './SimpleViewsBarChart'; // Use this -import LoadingSpinner from './LoadingSpinner'; +import SimpleVisitsBarChart from './SimpleVisitsBarChart'; +import SimpleViewsBarChart from './SimpleViewsBarChart'; import CountUpTimer from './CountUpTimer'; import DataTable from './DataTable'; @@ -38,7 +37,7 @@ const Dashboard: React.FC = () => { } const apps = await response.json(); - console.log('๐Ÿ“ฑ Fetched applications:', apps.length); + // console.log('๐Ÿ“ฑ Fetched applications:', apps.length); setApplications(apps); @@ -49,7 +48,7 @@ const Dashboard: React.FC = () => { }); setApplicationMap(appMap); - console.log('๐Ÿ“ฑ Created application map with', Object.keys(appMap).length, 'entries'); + // console.log('๐Ÿ“ฑ Created application map with', Object.keys(appMap).length, 'entries'); } catch (error) { console.error('Error fetching applications:', error); @@ -61,7 +60,7 @@ const Dashboard: React.FC = () => { try { const response = await fetch('/api/check-env'); const data = await response.json(); - console.log('Environment variables check:', data); + // console.log('Environment variables check:', data); alert(`API Key in .env.local: ${data.env_file.parsed_values.ACQUIA_API_KEY}\nAPI Key in process.env: ${data.process_env.ACQUIA_API_KEY}\nExact match: ${data.comparison.exact_match.ACQUIA_API_KEY}`); } catch (error) { console.error('Error checking environment variables:', error); @@ -95,7 +94,7 @@ const Dashboard: React.FC = () => { ...(dateTo && { to: dateTo }), }); - console.log('๐Ÿ”„ Fetching data with params:', { subscriptionUuid, dateFrom, dateTo }); + // console.log('๐Ÿ”„ Fetching data with params:', { subscriptionUuid, dateFrom, dateTo }); // Fetch visits data setLoadingStep('Fetching visits data from Acquia API...'); @@ -108,7 +107,7 @@ const Dashboard: React.FC = () => { } const visitsResult = await visitsResponse.json(); - console.log('๐Ÿ“Š Received visits result with length:', Array.isArray(visitsResult) ? visitsResult.length : 'not an array'); + // console.log('๐Ÿ“Š Received visits result with length:', Array.isArray(visitsResult) ? visitsResult.length : 'not an array'); // Fetch views data setLoadingStep('Fetching views data from Acquia API...'); const viewsResponse = await fetch(`/api/acquia/views?${params}`); @@ -120,7 +119,7 @@ const Dashboard: React.FC = () => { } const viewsResult = await viewsResponse.json(); - console.log('๐Ÿ“ˆ Received views result with length:', Array.isArray(viewsResult) ? viewsResult.length : 'not an array'); + // console.log('๐Ÿ“ˆ Received views result with length:', Array.isArray(viewsResult) ? viewsResult.length : 'not an array'); setLoadingStep('Processing data...'); // Handle different response formats @@ -331,23 +330,35 @@ const Dashboard: React.FC = () => { (Note that it can take several minutes to fetch data from the Acquia API.)

-
- -
{/* Data Display Section */}
+ {/* Data Tables Section */} +
+ ({ + rank: index + 1, + name: app.name, + value: app.views, + uuid: app.uuid, + }))} + valueLabel="Views" + /> + ({ + rank: index + 1, + name: app.name, + value: app.visits, + uuid: app.uuid, + }))} + valueLabel="Visits" + /> +
+ {/* Views Section */}

@@ -365,18 +376,6 @@ const Dashboard: React.FC = () => { ({ name: app.name, value: app.views, uuid: app.uuid }))} />

-
- ({ - rank: index + 1, - name: app.name, - value: app.views, - uuid: app.uuid, - }))} - valueLabel="Views" - /> -
{/* Visits Section */}
@@ -395,18 +394,6 @@ const Dashboard: React.FC = () => { ({ name: app.name, value: app.visits, uuid: app.uuid }))} />
-
- ({ - rank: index + 1, - name: app.name, - value: app.visits, - uuid: app.uuid, - }))} - valueLabel="Visits" - /> -
{loading && ( diff --git a/components/LoadingSpinner.tsx b/components/LoadingSpinner.tsx deleted file mode 100644 index a9f1930..0000000 --- a/components/LoadingSpinner.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const LoadingSpinner: React.FC = () => { - return ( -
-
-
- ); -}; - -export default LoadingSpinner; \ No newline at end of file diff --git a/components/SimpleVisitsPieChart.tsx b/components/SimpleVisitsPieChart.tsx deleted file mode 100644 index d7fca64..0000000 --- a/components/SimpleVisitsPieChart.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client'; - -import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'; - -interface SimpleVisitsPieChartProps { - data: { name: string; value: number }[]; -} - -const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D']; - -const SimpleVisitsPieChart: React.FC = ({ data }) => { - console.log('SimpleVisitsPieChart data:', data); - - return ( -
-

- Simple Visits Pie Chart -

- - - `${name}: ${value}`} - > - {data.map((entry, index) => ( - - ))} - - - -
- ); -}; - -export default SimpleVisitsPieChart; \ No newline at end of file diff --git a/components/TestChart.tsx b/components/TestChart.tsx deleted file mode 100644 index f0fdbc3..0000000 --- a/components/TestChart.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client'; - -import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'; - -const TestChart = () => { - const data = [ - { name: 'Group A', value: 400 }, - { name: 'Group B', value: 300 }, - { name: 'Group C', value: 300 }, - { name: 'Group D', value: 200 }, - ]; - - const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']; - - return ( -
-

Test Chart

- - - - {data.map((entry, index) => ( - - ))} - - - -
- ); -}; - -export default TestChart; \ No newline at end of file diff --git a/components/ViewsBarChart.tsx b/components/ViewsBarChart.tsx deleted file mode 100644 index 16009bc..0000000 --- a/components/ViewsBarChart.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts'; - -interface SummarizedData { - name: string; - value: number; - uuid: string; -} - -interface ViewsBarChartProps { - data: SummarizedData[]; -} - -const COLORS = ['#8884D8', '#82CA9D', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA']; - -const ViewsBarChart: React.FC = ({ data }) => { - const [chartData, setChartData] = useState([]); - const [isMounted, setIsMounted] = useState(false); - - useEffect(() => { - setIsMounted(true); - }, []); - - useEffect(() => { - if (!isMounted || !data) return; - - try { - // Data is already summarized. We just filter and sort it. - const filteredData = [...data] - .filter(item => item.value > 0) - .sort((a, b) => b.value - a.value); - - setChartData(filteredData); - - } catch (error) { - console.error('โŒ Error preparing views bar chart data:', error); - setChartData([]); - } - }, [data, isMounted]); - - if (!isMounted) { - return
Loading chart...
; - } - - if (chartData.length === 0) { - return ( -
-
No views data to display
-
- ); - } - - return ( -
- - - - - - [value.toLocaleString(), 'Views']} cursor={{fill: 'rgba(206, 206, 206, 0.2)'}} /> - - {chartData.map((entry, index) => ( - - ))} - - - -
- ); -}; - -export default ViewsBarChart; \ No newline at end of file diff --git a/components/VisitsBarChart.tsx b/components/VisitsBarChart.tsx deleted file mode 100644 index 08d2681..0000000 --- a/components/VisitsBarChart.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts'; - -interface SummarizedData { - name: string; - value: number; - uuid: string; -} - -interface VisitsBarChartProps { - data: SummarizedData[]; -} - -const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA']; - -const VisitsBarChart: React.FC = ({ data }) => { - const [chartData, setChartData] = useState([]); - const [isMounted, setIsMounted] = useState(false); - - useEffect(() => { - setIsMounted(true); - }, []); - - useEffect(() => { - if (!isMounted || !data) return; - - try { - // Data is already summarized. We just filter and sort it. - const filteredData = [...data] - .filter(item => item.value > 0) - .sort((a, b) => b.value - a.value); - - setChartData(filteredData); - - } catch (error) { - console.error('โŒ Error preparing visits bar chart data:', error); - setChartData([]); - } - }, [data, isMounted]); - - if (!isMounted) { - return
Loading chart...
; - } - - if (chartData.length === 0) { - return ( -
-
No visits data to display
-
- ); - } - - return ( -
- - - - - - [value.toLocaleString(), 'Visits']} cursor={{fill: 'rgba(206, 206, 206, 0.2)'}} /> - - {chartData.map((entry, index) => ( - - ))} - - - -
- ); -}; - -export default VisitsBarChart; \ No newline at end of file diff --git a/components/VisitsPieChart.tsx b/components/VisitsPieChart.tsx index 9b28c8c..ffd281a 100644 --- a/components/VisitsPieChart.tsx +++ b/components/VisitsPieChart.tsx @@ -72,7 +72,7 @@ const VisitsPieChart: React.FC = ({ data }) => { useEffect(() => { if (!isMounted || !data) return; - console.log('๐ŸŽฏ VisitsPieChart receiving pre-summarized data:', data.length, 'records'); + // console.log('๐ŸŽฏ VisitsPieChart receiving pre-summarized data:', data.length, 'records'); try { // Data is already summarized. We just add colors and sort. @@ -89,7 +89,7 @@ const VisitsPieChart: React.FC = ({ data }) => { const total = filteredData.reduce((sum, item) => sum + item.value, 0); - console.log(`๐ŸŽฏ Prepared pie chart data: ${filteredData.length} applications, ${total.toLocaleString()} total visits`); + // console.log(`๐ŸŽฏ Prepared pie chart data: ${filteredData.length} applications, ${total.toLocaleString()} total visits`); setChartData(filteredData); setTotalVisits(total); diff --git a/lib/acquia-api-fixed.ts b/lib/acquia-api-fixed.ts deleted file mode 100644 index ebc1e5f..0000000 --- a/lib/acquia-api-fixed.ts +++ /dev/null @@ -1,682 +0,0 @@ -import axios from 'axios'; - -export interface VisitsData { - applicationUuid: string; - applicationName?: string; - environmentUuid?: string; - environmentName?: string; - visits: number; - date: string; -} - -export interface ViewsData { - applicationUuid: string; - applicationName?: string; - environmentUuid?: string; - environmentName?: string; - views: number; - date: string; -} - -export interface Application { - uuid: string; - name: string; - subscription?: { - uuid: string; - name: string; - }; - environments?: { - uuid: string; - name: string; - }[]; -} - -export interface AcquiaApiConfig { - baseUrl: string; - authUrl: string; - apiKey: string; - apiSecret: string; -} - -export interface FetchProgress { - step: string; - currentPage?: number; - totalPages?: number; - itemsCollected?: number; -} - -class AcquiaApiServiceFixed { - private config: AcquiaApiConfig; - private accessToken: string | null = null; - private readonly AUTH_TIMEOUT = 120000; - private readonly API_TIMEOUT = 120000; - private progressCallback?: (progress: FetchProgress) => void; - - constructor(config: AcquiaApiConfig) { - this.config = config; - - console.log('๐Ÿ”ง Initializing Acquia API Service...'); - } - setProgressCallback(callback: (progress: FetchProgress) => void) { - this.progressCallback = callback; - } - - private reportProgress(progress: FetchProgress) { - if (this.progressCallback) { - this.progressCallback(progress); - } - console.log('๐Ÿ“Š Progress:', progress); - } - - private async getAccessToken(): Promise { - if (this.accessToken) { - return this.accessToken; - } - - // Debug credentials - console.log('๐Ÿ” Debug API Key:', { - value: this.config.apiKey ? `${this.config.apiKey.substring(0, 8)}...` : 'missing', - length: this.config.apiKey?.length || 0, - hasQuotes: this.config.apiKey?.startsWith('"') && this.config.apiKey?.endsWith('"') - }); - - console.log('๐Ÿ” Debug API Secret:', { - preview: this.config.apiSecret ? `${this.config.apiSecret.substring(0, 8)}...` : 'missing', - length: this.config.apiSecret?.length || 0, - hasQuotes: this.config.apiSecret?.startsWith('"') && this.config.apiSecret?.endsWith('"') - }); - - // Clean the credentials - remove any quotes that might be present - let cleanApiKey = this.config.apiKey.replace(/^"|"$/g, '').trim(); - const cleanApiSecret = this.config.apiSecret.replace(/^"|"$/g, '').trim(); - - // Check if API key appears to be base64 encoded (common issue in some environments) - // If it starts with base64-like characters and doesn't look like a UUID, try decoding - if (cleanApiKey && !cleanApiKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) && - cleanApiKey.match(/^[A-Za-z0-9+/]+=*$/)) { - try { - const decodedKey = Buffer.from(cleanApiKey, 'base64').toString('utf-8'); - // Check if decoded value looks like a UUID - if (decodedKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) { - console.log('๐Ÿ”ง Detected base64-encoded API key, using decoded value'); - cleanApiKey = decodedKey; - } - } catch (error) { - // If decoding fails, use original value - console.log('โš ๏ธ Failed to decode suspected base64 API key, using original value'); - } - } - - console.log('๐Ÿ” Using cleaned credentials:', { - keyLength: cleanApiKey.length, - secretLength: cleanApiSecret.length - }); - - const authUrl = `${this.config.authUrl}/auth/oauth/token`; - - // Try different authentication methods - const authMethods = [ - // Method 1: Basic Auth - async () => { - console.log('๐Ÿ” Trying Basic Auth method...'); - const credentials = Buffer.from(`${cleanApiKey}:${cleanApiSecret}`).toString('base64'); - const response = await axios({ - method: 'POST', - url: authUrl, - headers: { - 'Authorization': `Basic ${credentials}`, - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': '*/*', - }, - data: 'grant_type=client_credentials', - timeout: this.AUTH_TIMEOUT, - validateStatus: () => true, - }); - - console.log('๐Ÿ“ฅ Basic Auth response status:', response.status); - if (response.status === 200 && response.data?.access_token) { - return response.data.access_token; - } - throw new Error(`Basic Auth failed: ${response.status} - ${JSON.stringify(response.data)}`); - }, - - // Method 2: Form parameters - async () => { - console.log('๐Ÿ” Trying Form Parameters method...'); - const formData = new URLSearchParams(); - formData.append('grant_type', 'client_credentials'); - formData.append('client_id', cleanApiKey); - formData.append('client_secret', cleanApiSecret); - - const response = await axios({ - method: 'POST', - url: authUrl, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': '*/*', - }, - data: formData.toString(), - timeout: this.AUTH_TIMEOUT, - validateStatus: () => true, - }); - - console.log('๐Ÿ“ฅ Form Parameters response status:', response.status); - if (response.status === 200 && response.data?.access_token) { - return response.data.access_token; - } - throw new Error(`Form Parameters failed: ${response.status} - ${JSON.stringify(response.data)}`); - }, - - // Method 3: Use correct client ID format (if UUID is in different format) - async () => { - console.log('๐Ÿ” Trying with alternate client ID format...'); - - // Try with a UUID format if the key is not already in UUID format - const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cleanApiKey); - const clientId = isUuid - ? cleanApiKey - : 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3'; // fallback to known working UUID - - const formData = new URLSearchParams(); - formData.append('grant_type', 'client_credentials'); - formData.append('client_id', clientId); - formData.append('client_secret', cleanApiSecret); - - const response = await axios({ - method: 'POST', - url: authUrl, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': '*/*', - }, - data: formData.toString(), - timeout: this.AUTH_TIMEOUT, - validateStatus: () => true, - }); - - console.log('๐Ÿ“ฅ Alternate client ID response status:', response.status); - if (response.status === 200 && response.data?.access_token) { - return response.data.access_token; - } - throw new Error(`Alternate client ID failed: ${response.status} - ${JSON.stringify(response.data)}`); - } - ]; - - // Try each authentication method - let lastError: Error | null = null; - for (const method of authMethods) { - try { - const token = await method(); - this.accessToken = token; - console.log('โœ… Successfully authenticated!'); - return token; - } catch (error) { - lastError = error instanceof Error ? error : new Error(String(error)); - console.warn('โš ๏ธ Auth method failed:', lastError.message); - // Continue to next method - } - } - - // If we get here, all methods failed - console.error('โŒ All authentication methods failed'); - throw lastError || new Error('Failed to authenticate with Acquia API'); - } - - private async makeAuthenticatedRequest(endpoint: string) { - const token = await this.getAccessToken(); - const fullUrl = `${this.config.baseUrl}${endpoint}`; - try { - const response = await axios.get(fullUrl, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': '*/*', - }, - timeout: this.API_TIMEOUT, - }); - - return response; - } catch (error) { - if (axios.isAxiosError(error)) { - if (error.response?.status === 401) { - console.log('๐Ÿ”„ Token expired, retrying...'); - this.accessToken = null; - - const newToken = await this.getAccessToken(); - return axios.get(fullUrl, { - headers: { - 'Authorization': `Bearer ${newToken}`, - 'Accept': '*/*', - }, - timeout: this.API_TIMEOUT, - }); - } - } - - throw error; - } - } - - async getApplications(): Promise { - try { - console.log(`๐Ÿ” Fetching all applications`); - - const response = await this.makeAuthenticatedRequest('/applications'); - - console.log('โœ… Applications API Response Status:', response.status); - - let applications: Application[] = []; - - if (response.data._embedded?.items) { - applications = response.data._embedded.items.map((item: any) => ({ - uuid: item.uuid, - name: item.name || `App ${item.uuid.substring(0, 8)}`, - subscription: item.subscription ? { - uuid: item.subscription.uuid, - name: item.subscription.name - } : undefined, - environments: item._embedded?.environments?.map((env: any) => ({ - uuid: env.uuid, - name: env.name - })) - })); - - console.log(`โœ… Extracted ${applications.length} applications`); - } else { - console.warn('โš ๏ธ No applications found in response'); - } - - return applications; - } catch (error) { - console.error('โŒ Error fetching applications:', error); - throw error; - } - } - - private buildFilterParam(from?: string, to?: string): string { - if (!from && !to) { - console.log('๐Ÿ“… No date range specified, API will return default data'); - return ''; - } - - console.log(`๐Ÿ“… Building filter for date range: ${from} to ${to}`); - - // Convert YYYY-MM-DD format to the exact format the API expects - const formatDateForApi = (dateStr: string, isEndDate: boolean = false): string => { - // If it's already in the correct ISO format, return as-is - if (dateStr.includes('T') && dateStr.includes('Z')) { - return dateStr; - } - - // Convert YYYY-MM-DD to the exact format Acquia expects - let isoDate: string; - if (dateStr.includes('T')) { - // Already has time component, just ensure it ends with Z - isoDate = dateStr.endsWith('Z') ? dateStr : `${dateStr}Z`; - } else { - // Simple date format, add appropriate time - if (isEndDate) { - // For end date, use end of day - isoDate = `${dateStr}T23:59:59.000Z`; - } else { - // For start date, use beginning of day - isoDate = `${dateStr}T00:00:00.000Z`; - } - } - - console.log(`๐Ÿ“… Formatted ${dateStr} (end=${isEndDate}) -> ${isoDate}`); - return isoDate; - }; - - let filterParts: string[] = []; - - if (from) { - const fromDate = formatDateForApi(from, false); - filterParts.push(`from=${fromDate}`); - } - - if (to) { - const toDate = formatDateForApi(to, true); - filterParts.push(`to=${toDate}`); - } - - const filterString = filterParts.join(','); - console.log(`๐Ÿ“… Final filter parameter: ${filterString}`); - return filterString; - } - - private parseApplicationData(responseData: any, dataType: 'visits' | 'views'): VisitsData[] | ViewsData[] { - console.log('\n๐Ÿ” PARSING ACQUIA API RESPONSE - CORRECT ASSOCIATION'); - console.log('๐Ÿ“Š Response top-level keys:', Object.keys(responseData)); - - if (!responseData._embedded) { - console.warn('โš ๏ธ No _embedded found in response'); - return []; - } - - console.log('๐Ÿ“Š _embedded keys:', Object.keys(responseData._embedded)); - - if (!responseData._embedded.items || !Array.isArray(responseData._embedded.items)) { - console.warn('โš ๏ธ No _embedded.items array found in response'); - return []; - } - - const items = responseData._embedded.items; - console.log(`๐Ÿ“‹ Found ${items.length} items in _embedded.items`); - - const parsedVisitsData: VisitsData[] = []; - const parsedViewsData: ViewsData[] = []; - - items.forEach((item: any, itemIndex: number) => { - console.log(`\n๐Ÿข === PROCESSING ITEM ${itemIndex} (One Application) ===`); - console.log(`๐Ÿ“‹ Item structure: hasDatapoints=${!!item.datapoints}, datapointsCount=${item.datapoints?.length || 0}, hasMetadata=${!!item.metadata}, metadataKeys=${item.metadata ? JSON.stringify(Object.keys(item.metadata)) : '[]'}`); - // FIRST: Extract the application metadata for this entire item - let applicationUuid = ''; - let applicationName = ''; - let environmentUuids: string[] = []; - let environmentNames: string[] = []; - - console.log(`๐Ÿ“‹ Extracting metadata for item ${itemIndex}...`); - - // Get application info from metadata.application.uuids[0] - if (item.metadata?.application?.uuids && Array.isArray(item.metadata.application.uuids)) { - applicationUuid = item.metadata.application.uuids[0] || ''; - console.log(` ๐Ÿ†” Application UUID: ${applicationUuid}`); - } else { - console.log(` โŒ No application UUID found in metadata for item ${itemIndex}`); - if (item.metadata) { - console.log(` ๐Ÿ” Available metadata: ${JSON.stringify(item.metadata, null, 2)}`); - } else { - console.log(` ๐Ÿ” No metadata available`); - } - } - - // Get application name from metadata.application.names[0] - if (item.metadata?.application?.names && Array.isArray(item.metadata.application.names)) { - applicationName = item.metadata.application.names[0] || ''; - console.log(` ๐Ÿ“ Application name: ${applicationName}`); - } - - // If no name found, generate one from UUID - if (!applicationName && applicationUuid) { - applicationName = `App ${applicationUuid.substring(0, 8)}`; - console.log(` ๐Ÿ“ Generated application name: ${applicationName}`); - } - - // Get environment info if available - if (item.metadata?.environment) { - if (item.metadata.environment.uuids && Array.isArray(item.metadata.environment.uuids)) { - environmentUuids = item.metadata.environment.uuids; - console.log(` ๐ŸŒ Environment UUIDs (${environmentUuids.length}): ${JSON.stringify(environmentUuids)}`); - } - - if (item.metadata.environment.names && Array.isArray(item.metadata.environment.names)) { - environmentNames = item.metadata.environment.names; - console.log(` ๐ŸŒ Environment names (${environmentNames.length}): ${JSON.stringify(environmentNames)}`); - } - } - - // SECOND: Process ALL datapoints for this ONE application - if (!item.datapoints || !Array.isArray(item.datapoints)) { - console.log(` โš ๏ธ No datapoints found for application ${applicationUuid} (item ${itemIndex})`); - return; // Skip this item - } - - console.log(` ๐Ÿ“ˆ Processing ${item.datapoints.length} datapoints for application: ${applicationName} (${applicationUuid})`); - - item.datapoints.forEach((datapoint: any, dpIndex: number) => { - console.log(` ๐Ÿ“ Datapoint ${dpIndex} for ${applicationName}: ${JSON.stringify(datapoint, null, 2)}`); - let date = ''; - let value = 0; - - // Handle array format: ["2025-04-15T00:00:00+00:00", "1124"] - if (Array.isArray(datapoint) && datapoint.length >= 2) { - date = datapoint[0]; - // Handle both string and number values - value = typeof datapoint[1] === 'string' ? parseInt(datapoint[1]) || 0 : datapoint[1] || 0; - console.log(` ๐Ÿ“… Date: ${date}`); - console.log(` ๐Ÿ”ข Value: ${value} ${dataType}`); - } - // Handle object format (fallback) - else if (typeof datapoint === 'object') { - date = datapoint.datetime || datapoint.date || datapoint.timestamp || ''; - value = parseInt(datapoint.value) || parseInt(datapoint[dataType]) || 0; - console.log(` ๐Ÿ“… Date (object): ${date}`); - console.log(` ๐Ÿ”ข Value (object): ${value} ${dataType}`); - } else { - console.log(` โš ๏ธ Unexpected datapoint format: ${typeof datapoint}, ${String(datapoint)}`); - return; // Skip this datapoint - } - - // Create record for this datapoint - ALL belong to the SAME application - if (applicationUuid && date) { - // Use the first environment or create a general record - const environmentUuid = environmentUuids[0] || ''; - const environmentName = environmentNames[0] || (environmentUuid ? `Env ${environmentUuid.substring(0, 8)}` : 'All Environments'); - const baseData = { - applicationUuid, - applicationName, - environmentUuid, - environmentName, - date - }; - - if (dataType === 'visits') { - const visitData: VisitsData = { - ...baseData, - visits: value - }; - parsedVisitsData.push(visitData); - console.log(` โœ… Created visits record: ${value} visits for ${applicationName} on ${date}`); - } else { - const viewData: ViewsData = { - ...baseData, - views: value - }; - parsedViewsData.push(viewData); - console.log(` โœ… Created views record: ${value} views for ${applicationName} on ${date}`); - } - } else { - console.log(` โš ๏ธ Skipping datapoint - missing required data:`); - console.log(` - applicationUuid: ${applicationUuid || 'MISSING'}`); - console.log(` - date: ${date || 'MISSING'}`); - } - }); - - console.log(` ๐Ÿ“Š Completed processing ${item.datapoints.length} datapoints for ${applicationName}`); - }); - - // Return the correct array based on dataType - const parsedData = dataType === 'visits' ? parsedVisitsData : parsedViewsData; - - console.log(`\nโœ… PARSING COMPLETE`); - console.log(`๐Ÿ“Š Total ${dataType} records created: ${parsedData.length}`); - - // Enhanced summary statistics - const totalValue = parsedData.reduce((sum, item) => { - return sum + (dataType === 'visits' ? (item as VisitsData).visits : (item as ViewsData).views); - }, 0); - - const applicationSummary = parsedData.reduce((acc, item) => { - const appKey = item.applicationUuid; - if (!acc[appKey]) { - acc[appKey] = { - name: item.applicationName, - uuid: item.applicationUuid, - environments: new Set(), - totalValue: 0, - datapoints: 0, - dateRange: { min: item.date, max: item.date } - }; - } - acc[appKey].environments.add(item.environmentName || 'Unknown'); - acc[appKey].totalValue += (dataType === 'visits' ? (item as VisitsData).visits : (item as ViewsData).views); - acc[appKey].datapoints += 1; - - // Track date range - if (item.date < acc[appKey].dateRange.min) acc[appKey].dateRange.min = item.date; - if (item.date > acc[appKey].dateRange.max) acc[appKey].dateRange.max = item.date; - - return acc; - }, {} as Record); - - console.log(`๐Ÿ“Š Total ${dataType}: ${totalValue.toLocaleString()}`); - console.log(`๐Ÿ“Š Applications found: ${Object.keys(applicationSummary).length}`); - Object.entries(applicationSummary).forEach(([uuid, summary]: [string, any]) => { - console.log(` โ€ข ${summary.name} (${uuid.substring(0, 8)}...): ${summary.totalValue.toLocaleString()} ${dataType}, ${summary.datapoints} datapoints`); - }); - return parsedData; - } - - private async fetchAllPages( - baseEndpoint: string, - dataType: 'visits' | 'views', - subscriptionUuid: string, - from?: string, - to?: string - ): Promise { - let allData: T[] = []; - let currentPage = 1; - let totalPages = 1; - let hasMorePages = true; - - // Build the filter parameter with corrected date formatting - const filterParam = this.buildFilterParam(from, to); - console.log(`๐Ÿ” Date range requested: ${from} to ${to}`); - console.log(`๐Ÿ” Filter parameter: ${filterParam}`); - - while (hasMorePages) { - try { - const params = new URLSearchParams(); - - // Add filter parameter if we have date range - if (filterParam) { - params.append('filter', filterParam); - console.log(`๐Ÿ“… Added filter parameter to request`); - } else { - console.log(`โš ๏ธ No filter parameter - API will return default date range`); - } - - // Add resolution parameter (day for visits, month for views as per your examples) - // const resolution = dataType === 'visits' ? 'day' : 'month'; - const resolution = 'day'; - params.append('resolution', resolution); - console.log(`๐Ÿ“Š Using resolution: ${resolution}`); - - // Add pagination if needed - if (currentPage > 1) { - params.append('page', currentPage.toString()); - } - - const fullEndpoint = `${baseEndpoint}?${params.toString()}`; - this.reportProgress({ - step: `Fetching ${dataType} data (page ${currentPage})...`, - currentPage, - totalPages: totalPages > 1 ? totalPages : undefined, - itemsCollected: allData.length - }); - - console.log(`๐Ÿ“ก Making request to: ${fullEndpoint}`); - console.log(`๐Ÿ“ก Full URL parameters:`, params.toString()); - - const startTime = Date.now(); - const response = await this.makeAuthenticatedRequest(fullEndpoint); - const endTime = Date.now(); - - console.log(`โœ… Request completed in ${endTime - startTime}ms`); - console.log(`๐Ÿ“Š Response status: ${response.status}`); - - // Log some response details to debug date issues - if (response.data._embedded?.items?.length > 0) { - const firstItem = response.data._embedded.items[0]; - if (firstItem.datapoints?.length > 0) { - const firstDatapoint = firstItem.datapoints[0]; - const lastDatapoint = firstItem.datapoints[firstItem.datapoints.length - 1]; - console.log(`๐Ÿ“… API returned data from ${firstDatapoint[0]} to ${lastDatapoint[0]}`); - console.log(`๐Ÿ“Š Total datapoints in first item: ${firstItem.datapoints.length}`); - } - } - - const pageData = this.parseApplicationData(response.data, dataType) as T[]; - - // Log date range of parsed data - if (pageData.length > 0) { - const dates = pageData.map(item => item.date).filter(Boolean).sort(); - if (dates.length > 0) { - console.log(`๐Ÿ“… Parsed data date range: ${dates[0]} to ${dates[dates.length - 1]}`); -} - } - - allData = allData.concat(pageData); - - // Check pagination - const pageInfo = response.data.page; - if (pageInfo) { - totalPages = pageInfo.totalPages || pageInfo.total_pages || 1; - hasMorePages = currentPage < totalPages; - console.log(`๐Ÿ“„ Pagination: page ${currentPage} of ${totalPages}`); - } else { - const links = response.data._links; - hasMorePages = !!(links && links.next); - if (links?.next) { - console.log(`๐Ÿ“„ Found next link: ${links.next.href}`); - } else { - console.log(`๐Ÿ“„ No more pages found`); - } - } - - currentPage++; - if (currentPage > 100) { - console.warn('โš ๏ธ Stopping after 100 pages to prevent infinite loop'); - break; - } - - if (hasMorePages) { - console.log('โฑ๏ธ Waiting 500ms before next request...'); - await new Promise(resolve => setTimeout(resolve, 500)); - } - - } catch (error) { - console.error(`โŒ Error fetching page ${currentPage}:`, error); - - if (error instanceof Error && error.message.includes('timeout')) { - throw new Error(`Request timed out after ${this.API_TIMEOUT / 1000} seconds. Try a smaller date range or check your network connection.`); - } - - throw error; - } - } - - this.reportProgress({ - step: `Completed! Collected ${allData.length} ${dataType} records.`, - currentPage: currentPage - 1, - totalPages, - itemsCollected: allData.length - }); - - console.log(`๐ŸŽ‰ Successfully fetched ${allData.length} ${dataType} records from ${currentPage - 1} pages`); - - // Final summary of date range - if (allData.length > 0) { - const dates = allData.map(item => item.date).filter(Boolean).sort(); - if (dates.length > 0) { - console.log(`๐Ÿ“… Final data covers: ${dates[0]} to ${dates[dates.length - 1]}`); - } - } - - return allData; - } - - async getVisitsDataByApplication(subscriptionUuid: string, from?: string, to?: string): Promise { - const baseEndpoint = `/subscriptions/${subscriptionUuid}/metrics/usage/visits-by-application`; - console.log(`๐Ÿšถ Fetching visits data with resolution=day for date range: ${from || 'no start'} to ${to || 'no end'}`); - return this.fetchAllPages(baseEndpoint, 'visits', subscriptionUuid, from, to); - } - - async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: string): Promise { - const baseEndpoint = `/subscriptions/${subscriptionUuid}/metrics/usage/views-by-application`; - console.log(`๐Ÿ‘๏ธ Fetching views data with resolution=month for date range: ${from || 'no start'} to ${to || 'no end'}`); - return this.fetchAllPages(baseEndpoint, 'views', subscriptionUuid, from, to); - } -} - -export default AcquiaApiServiceFixed; \ No newline at end of file diff --git a/lib/acquia-api.ts b/lib/acquia-api.ts index 91d1f70..4c12d44 100644 --- a/lib/acquia-api.ts +++ b/lib/acquia-api.ts @@ -1,15 +1,34 @@ import axios from 'axios'; export interface VisitsData { - date: string; - visits: number; applicationUuid: string; + applicationName?: string; + environmentUuid?: string; + environmentName?: string; + visits: number; + date: string; } export interface ViewsData { - date: string; - views: number; applicationUuid: string; + applicationName?: string; + environmentUuid?: string; + environmentName?: string; + views: number; + date: string; +} + +export interface Application { + uuid: string; + name: string; + subscription?: { + uuid: string; + name: string; + }; + environments?: { + uuid: string; + name: string; + }[]; } export interface AcquiaApiConfig { @@ -19,13 +38,34 @@ export interface AcquiaApiConfig { apiSecret: string; } -class AcquiaApiService { +export interface FetchProgress { + step: string; + currentPage?: number; + totalPages?: number; + itemsCollected?: number; +} + +class AcquiaApiServiceFixed { private config: AcquiaApiConfig; private accessToken: string | null = null; + private readonly AUTH_TIMEOUT = 120000; + private readonly API_TIMEOUT = 120000; + private progressCallback?: (progress: FetchProgress) => void; constructor(config: AcquiaApiConfig) { this.config = config; - console.log('๐Ÿ”ง Initializing Acquia API Service...'); + + // console.log('๐Ÿ”ง Initializing Acquia API Service...'); + } + setProgressCallback(callback: (progress: FetchProgress) => void) { + this.progressCallback = callback; + } + + private reportProgress(progress: FetchProgress) { + if (this.progressCallback) { + this.progressCallback(progress); + } + // console.log('๐Ÿ“Š Progress:', progress); } private async getAccessToken(): Promise { @@ -33,152 +73,611 @@ class AcquiaApiService { return this.accessToken; } - const cleanApiKey = this.config.apiKey.trim(); - const cleanApiSecret = this.config.apiSecret.trim(); - - console.log('๐Ÿ” Attempting authentication with corrected headers...'); + // Debug credentials + /* console.log('๐Ÿ” Debug API Key:', { + value: this.config.apiKey ? `${this.config.apiKey.substring(0, 8)}...` : 'missing', + length: this.config.apiKey?.length || 0, + hasQuotes: this.config.apiKey?.startsWith('"') && this.config.apiKey?.endsWith('"') + }); + + console.log('๐Ÿ” Debug API Secret:', { + preview: this.config.apiSecret ? `${this.config.apiSecret.substring(0, 8)}...` : 'missing', + length: this.config.apiSecret?.length || 0, + hasQuotes: this.config.apiSecret?.startsWith('"') && this.config.apiSecret?.endsWith('"') + });*/ + + // Clean the credentials - remove any quotes that might be present + let cleanApiKey = this.config.apiKey.replace(/^"|"$/g, '').trim(); + const cleanApiSecret = this.config.apiSecret.replace(/^"|"$/g, '').trim(); + + // Check if API key appears to be base64 encoded (common issue in some environments) + // If it starts with base64-like characters and doesn't look like a UUID, try decoding + if (cleanApiKey && !cleanApiKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) && + cleanApiKey.match(/^[A-Za-z0-9+/]+=*$/)) { + try { + const decodedKey = Buffer.from(cleanApiKey, 'base64').toString('utf-8'); + // Check if decoded value looks like a UUID + if (decodedKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) { + // console.log('๐Ÿ”ง Detected base64-encoded API key, using decoded value'); + cleanApiKey = decodedKey; + } + } catch (error) { + // If decoding fails, use original value + // console.log('โš ๏ธ Failed to decode suspected base64 API key, using original value'); + } + } + /** + console.log('๐Ÿ” Using cleaned credentials:', { + keyLength: cleanApiKey.length, + secretLength: cleanApiSecret.length + }); + */ + const authUrl = `${this.config.authUrl}/auth/oauth/token`; - // Try the methods most likely to work based on the header error + // Try different authentication methods const authMethods = [ - { - name: 'Form parameters, no Accept header', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - data: new URLSearchParams({ - 'grant_type': 'client_credentials', - 'client_id': cleanApiKey, - 'client_secret': cleanApiSecret - }).toString() - }, - { - name: 'Form parameters, Accept */*', + // Method 1: Basic Auth + async () => { + // console.log('๐Ÿ” Trying Basic Auth method...'); + const credentials = Buffer.from(`${cleanApiKey}:${cleanApiSecret}`).toString('base64'); + const response = await axios({ + method: 'POST', + url: authUrl, headers: { + 'Authorization': `Basic ${credentials}`, 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': '*/*', }, - data: new URLSearchParams({ - 'grant_type': 'client_credentials', - 'client_id': cleanApiKey, - 'client_secret': cleanApiSecret - }).toString() - }, - { - name: 'Basic Auth, no Accept header', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${Buffer.from(`${cleanApiKey}:${cleanApiSecret}`).toString('base64')}` + data: 'grant_type=client_credentials', + timeout: this.AUTH_TIMEOUT, + validateStatus: () => true, + }); + + // console.log('๐Ÿ“ฅ Basic Auth response status:', response.status); + if (response.status === 200 && response.data?.access_token) { + return response.data.access_token; + } + throw new Error(`Basic Auth failed: ${response.status} - ${JSON.stringify(response.data)}`); }, - data: 'grant_type=client_credentials' - } - ]; - for (const method of authMethods) { - try { - console.log(`๐Ÿ” Trying: ${method.name}`); + // Method 2: Form parameters + async () => { + // console.log('๐Ÿ” Trying Form Parameters method...'); + const formData = new URLSearchParams(); + formData.append('grant_type', 'client_credentials'); + formData.append('client_id', cleanApiKey); + formData.append('client_secret', cleanApiSecret); const response = await axios({ method: 'POST', url: authUrl, - headers: method.headers, - data: method.data, - timeout: 30000, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': '*/*', + }, + data: formData.toString(), + timeout: this.AUTH_TIMEOUT, validateStatus: () => true, - }); - - console.log(`๐Ÿ“ฅ ${method.name} response:`, response.status, response.data); + }); + // console.log('๐Ÿ“ฅ Form Parameters response status:', response.status); if (response.status === 200 && response.data?.access_token) { - this.accessToken = response.data.access_token; - console.log(`โœ… Successfully authenticated using: ${method.name}`); - return this.accessToken; + return response.data.access_token; } - } catch (error) { - console.log(`โŒ ${method.name} failed:`, error instanceof Error ? error.message : String(error)); + throw new Error(`Form Parameters failed: ${response.status} - ${JSON.stringify(response.data)}`); + }, + + // Method 3: Use correct client ID format (if UUID is in different format) + async () => { + // console.log('๐Ÿ” Trying with alternate client ID format...'); + + // Try with a UUID format if the key is not already in UUID format + const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cleanApiKey); + const clientId = isUuid + ? cleanApiKey + : 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3'; // fallback to known working UUID + + const formData = new URLSearchParams(); + formData.append('grant_type', 'client_credentials'); + formData.append('client_id', clientId); + formData.append('client_secret', cleanApiSecret); + + const response = await axios({ + method: 'POST', + url: authUrl, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': '*/*', + }, + data: formData.toString(), + timeout: this.AUTH_TIMEOUT, + validateStatus: () => true, + }); + + // console.log('๐Ÿ“ฅ Alternate client ID response status:', response.status); + if (response.status === 200 && response.data?.access_token) { + return response.data.access_token; } + throw new Error(`Alternate client ID failed: ${response.status} - ${JSON.stringify(response.data)}`); } + ]; - throw new Error('Failed to authenticate with any method'); + // Try each authentication method + let lastError: Error | null = null; + for (const method of authMethods) { + try { + const token = await method(); + this.accessToken = token; + // console.log('โœ… Successfully authenticated!'); + return token; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + console.warn('โš ๏ธ Auth method failed:', lastError.message); + // Continue to next method + } } + // If we get here, all methods failed + console.error('โŒ All authentication methods failed'); + throw lastError || new Error('Failed to authenticate with Acquia API'); + } + private async makeAuthenticatedRequest(endpoint: string) { const token = await this.getAccessToken(); const fullUrl = `${this.config.baseUrl}${endpoint}`; - - console.log(`๐Ÿ”— Making authenticated request to: ${fullUrl}`); try { const response = await axios.get(fullUrl, { headers: { 'Authorization': `Bearer ${token}`, - 'Accept': '*/*', // Use the working Accept header + 'Accept': '*/*', }, - timeout: 30000, + timeout: this.API_TIMEOUT, }); return response; } catch (error) { - if (axios.isAxiosError(error) && error.response?.status === 401) { - console.log('๐Ÿ”„ Token expired, clearing and retrying...'); - this.accessToken = null; - - const newToken = await this.getAccessToken(); - return axios.get(fullUrl, { - headers: { - 'Authorization': `Bearer ${newToken}`, - 'Accept': '*/*', - }, - timeout: 30000, - }); - } - + if (axios.isAxiosError(error)) { + if (error.response?.status === 401) { + // console.log('๐Ÿ”„ Token expired, retrying...'); + this.accessToken = null; + + const newToken = await this.getAccessToken(); + return axios.get(fullUrl, { + headers: { + 'Authorization': `Bearer ${newToken}`, + 'Accept': '*/*', + }, + timeout: this.API_TIMEOUT, + }); + } + } + throw error; + } } -} - async getVisitsData(subscriptionUuid: string, applicationUuid: string, from?: string, to?: string): Promise { + async getApplications(): Promise { try { - const params = new URLSearchParams(); - if (from) params.append('from', from); - if (to) params.append('to', to); - - const endpoint = `/subscriptions/${subscriptionUuid}/usage/visits-by-application/${applicationUuid}${params.toString() ? `?${params.toString()}` : ''}`; - - console.log(`๐Ÿ“Š Fetching visits data with endpoint: ${endpoint}`); - - const response = await this.makeAuthenticatedRequest(endpoint); - - console.log('โœ… Visits API Response Status:', response.status); - console.log('โœ… Visits API Response Data:', JSON.stringify(response.data, null, 2)); - - return response.data._embedded?.visits || response.data.visits || []; + // console.log(`๐Ÿ” Fetching all applications`); + + const response = await this.makeAuthenticatedRequest('/applications'); + + // console.log('โœ… Applications API Response Status:', response.status); + + let applications: Application[] = []; + + if (response.data._embedded?.items) { + applications = response.data._embedded.items.map((item: any) => ({ + uuid: item.uuid, + name: item.name || `App ${item.uuid.substring(0, 8)}`, + subscription: item.subscription ? { + uuid: item.subscription.uuid, + name: item.subscription.name + } : undefined, + environments: item._embedded?.environments?.map((env: any) => ({ + uuid: env.uuid, + name: env.name + })) + })); + + // console.log(`โœ… Extracted ${applications.length} applications`); + } else { + console.warn('โš ๏ธ No applications found in response'); + } + + return applications; } catch (error) { - console.error('โŒ Error fetching visits data:', error); + console.error('โŒ Error fetching applications:', error); throw error; } } - async getViewsData(subscriptionUuid: string, applicationUuid: string, from?: string, to?: string): Promise { - try { - const params = new URLSearchParams(); - if (from) params.append('from', from); - if (to) params.append('to', to); - - const endpoint = `/subscriptions/${subscriptionUuid}/usage/views-by-application/${applicationUuid}${params.toString() ? `?${params.toString()}` : ''}`; - - console.log(`๐Ÿ“ˆ Fetching views data with endpoint: ${endpoint}`); - - const response = await this.makeAuthenticatedRequest(endpoint); + private buildFilterParam(from?: string, to?: string): string { + if (!from && !to) { + // console.log('๐Ÿ“… No date range specified, API will return default data'); + return ''; + } + + // console.log(`๐Ÿ“… Building filter for date range: ${from} to ${to}`); + + // Convert YYYY-MM-DD format to the exact format the API expects + const formatDateForApi = (dateStr: string, isEndDate: boolean = false): string => { + // If it's already in the correct ISO format, return as-is + if (dateStr.includes('T') && dateStr.includes('Z')) { + return dateStr; + } + + // Convert YYYY-MM-DD to the exact format Acquia expects + let isoDate: string; + if (dateStr.includes('T')) { + // Already has time component, just ensure it ends with Z + isoDate = dateStr.endsWith('Z') ? dateStr : `${dateStr}Z`; + } else { + // Simple date format, add appropriate time + if (isEndDate) { + // For end date, use end of day + isoDate = `${dateStr}T23:59:59.000Z`; + } else { + // For start date, use beginning of day + isoDate = `${dateStr}T00:00:00.000Z`; + } + } + + // console.log(`๐Ÿ“… Formatted ${dateStr} (end=${isEndDate}) -> ${isoDate}`); + return isoDate; + }; + + let filterParts: string[] = []; + + if (from) { + const fromDate = formatDateForApi(from, false); + filterParts.push(`from=${fromDate}`); + } + + if (to) { + const toDate = formatDateForApi(to, true); + filterParts.push(`to=${toDate}`); + } + + const filterString = filterParts.join(','); + // console.log(`๐Ÿ“… Final filter parameter: ${filterString}`); + return filterString; + } + + private parseApplicationData(responseData: any, dataType: 'visits' | 'views'): VisitsData[] | ViewsData[] { + // console.log('\n๐Ÿ” PARSING ACQUIA API RESPONSE - CORRECT ASSOCIATION'); + // console.log('๐Ÿ“Š Response top-level keys:', Object.keys(responseData)); + + if (!responseData._embedded) { + console.warn('โš ๏ธ No _embedded found in response'); + return []; + } + + // console.log('๐Ÿ“Š _embedded keys:', Object.keys(responseData._embedded)); + + if (!responseData._embedded.items || !Array.isArray(responseData._embedded.items)) { + console.warn('โš ๏ธ No _embedded.items array found in response'); + return []; + } + + const items = responseData._embedded.items; + // console.log(`๐Ÿ“‹ Found ${items.length} items in _embedded.items`); + + const parsedVisitsData: VisitsData[] = []; + const parsedViewsData: ViewsData[] = []; + + items.forEach((item: any, itemIndex: number) => { + // console.log(`\n๐Ÿข === PROCESSING ITEM ${itemIndex} (One Application) ===`); + // console.log(`๐Ÿ“‹ Item structure: hasDatapoints=${!!item.datapoints}, datapointsCount=${item.datapoints?.length || 0}, hasMetadata=${!!item.metadata}, metadataKeys=${item.metadata ? JSON.stringify(Object.keys(item.metadata)) : '[]'}`); + // FIRST: Extract the application metadata for this entire item + let applicationUuid = ''; + let applicationName = ''; + let environmentUuids: string[] = []; + let environmentNames: string[] = []; - console.log('โœ… Views API Response Status:', response.status); - console.log('โœ… Views API Response Data:', JSON.stringify(response.data, null, 2)); + // console.log(`๐Ÿ“‹ Extracting metadata for item ${itemIndex}...`); + + // Get application info from metadata.application.uuids[0] + if (item.metadata?.application?.uuids && Array.isArray(item.metadata.application.uuids)) { + applicationUuid = item.metadata.application.uuids[0] || ''; + // console.log(` ๐Ÿ†” Application UUID: ${applicationUuid}`); + } else { + // console.log(` โŒ No application UUID found in metadata for item ${itemIndex}`); + if (item.metadata) { + // console.log(` ๐Ÿ” Available metadata: ${JSON.stringify(item.metadata, null, 2)}`); + } else { + // console.log(` ๐Ÿ” No metadata available`); + } + } + + // Get application name from metadata.application.names[0] + if (item.metadata?.application?.names && Array.isArray(item.metadata.application.names)) { + applicationName = item.metadata.application.names[0] || ''; + // console.log(` ๐Ÿ“ Application name: ${applicationName}`); + } - return response.data._embedded?.views || response.data.views || []; - } catch (error) { - console.error('โŒ Error fetching views data:', error); - throw error; + // If no name found, generate one from UUID + if (!applicationName && applicationUuid) { + applicationName = `App ${applicationUuid.substring(0, 8)}`; + // console.log(` ๐Ÿ“ Generated application name: ${applicationName}`); + } + + // Get environment info if available + if (item.metadata?.environment) { + if (item.metadata.environment.uuids && Array.isArray(item.metadata.environment.uuids)) { + environmentUuids = item.metadata.environment.uuids; + // console.log(` ๐ŸŒ Environment UUIDs (${environmentUuids.length}): ${JSON.stringify(environmentUuids)}`); + } + + if (item.metadata.environment.names && Array.isArray(item.metadata.environment.names)) { + environmentNames = item.metadata.environment.names; + // console.log(` ๐ŸŒ Environment names (${environmentNames.length}): ${JSON.stringify(environmentNames)}`); + } + } + + // SECOND: Process ALL datapoints for this ONE application + if (!item.datapoints || !Array.isArray(item.datapoints)) { + // console.log(` โš ๏ธ No datapoints found for application ${applicationUuid} (item ${itemIndex})`); + return; // Skip this item + } + + // console.log(` ๐Ÿ“ˆ Processing ${item.datapoints.length} datapoints for application: ${applicationName} (${applicationUuid})`); + + item.datapoints.forEach((datapoint: any, dpIndex: number) => { + // console.log(` ๐Ÿ“ Datapoint ${dpIndex} for ${applicationName}: ${JSON.stringify(datapoint, null, 2)}`); + let date = ''; + let value = 0; + + // Handle array format: ["2025-04-15T00:00:00+00:00", "1124"] + if (Array.isArray(datapoint) && datapoint.length >= 2) { + date = datapoint[0]; + // Handle both string and number values + value = typeof datapoint[1] === 'string' ? parseInt(datapoint[1]) || 0 : datapoint[1] || 0; + // console.log(` ๐Ÿ“… Date: ${date}`); + // console.log(` ๐Ÿ”ข Value: ${value} ${dataType}`); + } + // Handle object format (fallback) + else if (typeof datapoint === 'object') { + date = datapoint.datetime || datapoint.date || datapoint.timestamp || ''; + value = parseInt(datapoint.value) || parseInt(datapoint[dataType]) || 0; + // console.log(` ๐Ÿ“… Date (object): ${date}`); + // console.log(` ๐Ÿ”ข Value (object): ${value} ${dataType}`); + } else { + // console.log(` โš ๏ธ Unexpected datapoint format: ${typeof datapoint}, ${String(datapoint)}`); + return; // Skip this datapoint + } + + // Create record for this datapoint - ALL belong to the SAME application + if (applicationUuid && date) { + // Use the first environment or create a general record + const environmentUuid = environmentUuids[0] || ''; + const environmentName = environmentNames[0] || (environmentUuid ? `Env ${environmentUuid.substring(0, 8)}` : 'All Environments'); + const baseData = { + applicationUuid, + applicationName, + environmentUuid, + environmentName, + date + }; + + if (dataType === 'visits') { + const visitData: VisitsData = { + ...baseData, + visits: value + }; + parsedVisitsData.push(visitData); + // console.log(` โœ… Created visits record: ${value} visits for ${applicationName} on ${date}`); + } else { + const viewData: ViewsData = { + ...baseData, + views: value + }; + parsedViewsData.push(viewData); + // console.log(` โœ… Created views record: ${value} views for ${applicationName} on ${date}`); + } + } else { + // console.log(` โš ๏ธ Skipping datapoint - missing required data:`); + // console.log(` - applicationUuid: ${applicationUuid || 'MISSING'}`); + // console.log(` - date: ${date || 'MISSING'}`); + } + }); + + // console.log(` ๐Ÿ“Š Completed processing ${item.datapoints.length} datapoints for ${applicationName}`); + }); + + // Return the correct array based on dataType + const parsedData = dataType === 'visits' ? parsedVisitsData : parsedViewsData; + + // console.log(`\nโœ… PARSING COMPLETE`); + // console.log(`๐Ÿ“Š Total ${dataType} records created: ${parsedData.length}`); + + // Enhanced summary statistics + const totalValue = parsedData.reduce((sum, item) => { + return sum + (dataType === 'visits' ? (item as VisitsData).visits : (item as ViewsData).views); + }, 0); + + const applicationSummary = parsedData.reduce((acc, item) => { + const appKey = item.applicationUuid; + if (!acc[appKey]) { + acc[appKey] = { + name: item.applicationName, + uuid: item.applicationUuid, + environments: new Set(), + totalValue: 0, + datapoints: 0, + dateRange: { min: item.date, max: item.date } + }; + } + acc[appKey].environments.add(item.environmentName || 'Unknown'); + acc[appKey].totalValue += (dataType === 'visits' ? (item as VisitsData).visits : (item as ViewsData).views); + acc[appKey].datapoints += 1; + + // Track date range + if (item.date < acc[appKey].dateRange.min) acc[appKey].dateRange.min = item.date; + if (item.date > acc[appKey].dateRange.max) acc[appKey].dateRange.max = item.date; + + return acc; + }, {} as Record); + + // console.log(`๐Ÿ“Š Total ${dataType}: ${totalValue.toLocaleString()}`); + // console.log(`๐Ÿ“Š Applications found: ${Object.keys(applicationSummary).length}`); + Object.entries(applicationSummary).forEach(([uuid, summary]: [string, any]) => { + // console.log(` โ€ข ${summary.name} (${uuid.substring(0, 8)}...): ${summary.totalValue.toLocaleString()} ${dataType}, ${summary.datapoints} datapoints`); + }); + return parsedData; + } + + private async fetchAllPages( + baseEndpoint: string, + dataType: 'visits' | 'views', + subscriptionUuid: string, + from?: string, + to?: string + ): Promise { + let allData: T[] = []; + let currentPage = 1; + let totalPages = 1; + let hasMorePages = true; + + // Build the filter parameter with corrected date formatting + const filterParam = this.buildFilterParam(from, to); + // console.log(`๐Ÿ” Date range requested: ${from} to ${to}`); + // console.log(`๐Ÿ” Filter parameter: ${filterParam}`); + + while (hasMorePages) { + try { + const params = new URLSearchParams(); + + // Add filter parameter if we have date range + if (filterParam) { + params.append('filter', filterParam); + // console.log(`๐Ÿ“… Added filter parameter to request`); + } else { + // console.log(`โš ๏ธ No filter parameter - API will return default date range`); + } + + // Add resolution parameter (day for visits, month for views as per your examples) + // const resolution = dataType === 'visits' ? 'day' : 'month'; + const resolution = 'day'; + params.append('resolution', resolution); + // console.log(`๐Ÿ“Š Using resolution: ${resolution}`); + + // Add pagination if needed + if (currentPage > 1) { + params.append('page', currentPage.toString()); + } + + const fullEndpoint = `${baseEndpoint}?${params.toString()}`; + this.reportProgress({ + step: `Fetching ${dataType} data (page ${currentPage})...`, + currentPage, + totalPages: totalPages > 1 ? totalPages : undefined, + itemsCollected: allData.length + }); + + // console.log(`๐Ÿ“ก Making request to: ${fullEndpoint}`); + // console.log(`๐Ÿ“ก Full URL parameters:`, params.toString()); + + const startTime = Date.now(); + const response = await this.makeAuthenticatedRequest(fullEndpoint); + const endTime = Date.now(); + + // console.log(`โœ… Request completed in ${endTime - startTime}ms`); + // console.log(`๐Ÿ“Š Response status: ${response.status}`); + + // Log some response details to debug date issues + if (response.data._embedded?.items?.length > 0) { + const firstItem = response.data._embedded.items[0]; + if (firstItem.datapoints?.length > 0) { + const firstDatapoint = firstItem.datapoints[0]; + const lastDatapoint = firstItem.datapoints[firstItem.datapoints.length - 1]; + // console.log(`๐Ÿ“… API returned data from ${firstDatapoint[0]} to ${lastDatapoint[0]}`); + // console.log(`๐Ÿ“Š Total datapoints in first item: ${firstItem.datapoints.length}`); + } + } + + const pageData = this.parseApplicationData(response.data, dataType) as T[]; + + // Log date range of parsed data + if (pageData.length > 0) { + const dates = pageData.map(item => item.date).filter(Boolean).sort(); + if (dates.length > 0) { + // console.log(`๐Ÿ“… Parsed data date range: ${dates[0]} to ${dates[dates.length - 1]}`); +} + } + + allData = allData.concat(pageData); + + // Check pagination + const pageInfo = response.data.page; + if (pageInfo) { + totalPages = pageInfo.totalPages || pageInfo.total_pages || 1; + hasMorePages = currentPage < totalPages; + // console.log(`๐Ÿ“„ Pagination: page ${currentPage} of ${totalPages}`); + } else { + const links = response.data._links; + hasMorePages = !!(links && links.next); + if (links?.next) { + // console.log(`๐Ÿ“„ Found next link: ${links.next.href}`); + } else { + // console.log(`๐Ÿ“„ No more pages found`); + } + } + + currentPage++; + if (currentPage > 100) { + console.warn('โš ๏ธ Stopping after 100 pages to prevent infinite loop'); + break; + } + + if (hasMorePages) { + // console.log('โฑ๏ธ Waiting 500ms before next request...'); + await new Promise(resolve => setTimeout(resolve, 500)); + } + + } catch (error) { + console.error(`โŒ Error fetching page ${currentPage}:`, error); + + if (error instanceof Error && error.message.includes('timeout')) { + throw new Error(`Request timed out after ${this.API_TIMEOUT / 1000} seconds. Try a smaller date range or check your network connection.`); + } + + throw error; + } + } + + this.reportProgress({ + step: `Completed! Collected ${allData.length} ${dataType} records.`, + currentPage: currentPage - 1, + totalPages, + itemsCollected: allData.length + }); + + // console.log(`๐ŸŽ‰ Successfully fetched ${allData.length} ${dataType} records from ${currentPage - 1} pages`); + + // Final summary of date range + if (allData.length > 0) { + const dates = allData.map(item => item.date).filter(Boolean).sort(); + if (dates.length > 0) { + // console.log(`๐Ÿ“… Final data covers: ${dates[0]} to ${dates[dates.length - 1]}`); + } } + + return allData; + } + + async getVisitsDataByApplication(subscriptionUuid: string, from?: string, to?: string): Promise { + const baseEndpoint = `/subscriptions/${subscriptionUuid}/metrics/usage/visits-by-application`; + // console.log(`๐Ÿšถ Fetching visits data with resolution=day for date range: ${from || 'no start'} to ${to || 'no end'}`); + return this.fetchAllPages(baseEndpoint, 'visits', subscriptionUuid, from, to); + } + + async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: string): Promise { + const baseEndpoint = `/subscriptions/${subscriptionUuid}/metrics/usage/views-by-application`; + // console.log(`๐Ÿ‘๏ธ Fetching views data with resolution=month for date range: ${from || 'no start'} to ${to || 'no end'}`); + return this.fetchAllPages(baseEndpoint, 'views', subscriptionUuid, from, to); } } -export default AcquiaApiService; \ No newline at end of file +export default AcquiaApiServiceFixed; \ No newline at end of file diff --git a/lib/debug-credentials.ts b/lib/debug-credentials.ts deleted file mode 100644 index ae88e6b..0000000 --- a/lib/debug-credentials.ts +++ /dev/null @@ -1,51 +0,0 @@ -export function debugCredentials(apiKey: string, apiSecret: string) { - console.log('๐Ÿ” CREDENTIAL DEBUG ANALYSIS:'); - console.log(''); - - // Show what we received - console.log('๐Ÿ“ฅ Received API Key:', JSON.stringify(apiKey)); - console.log('๐Ÿ“ฅ Received API Secret preview:', JSON.stringify(apiSecret.substring(0, 20) + '...')); - console.log(''); - - // Show lengths - console.log('๐Ÿ“ API Key length:', apiKey.length); - console.log('๐Ÿ“ API Secret length:', apiSecret.length); - console.log(''); - - // Show what the correct UUID should look like - console.log('โœ… Expected format - UUID client_id: deed5eaf-98ba-4924-8747-1fb1fbd00bd3'); - console.log('โŒ Incorrect value found: 60L4E7s0AsSQCpSAs9zcb7RQwlIPD3lI78uYQRtjslLt4bOYuEige7qdoyMQtLfmfgTkoXJKJaqF'); - console.log(''); - - // Test if the incorrect value is some transformation of the correct one - const correctClientId = 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3'; - const incorrectValue = '60L4E7s0AsSQCpSAs9zcb7RQwlIPD3lI78uYQRtjslLt4bOYuEige7qdoyMQtLfmfgTkoXJKJaqF'; - - console.log('๐Ÿงช Testing transformations:'); - console.log('Base64 of correct UUID:', Buffer.from(correctClientId).toString('base64')); - console.log('Base64 of UUID:secret:', Buffer.from(`${correctClientId}:${apiSecret}`).toString('base64').substring(0, 70) + '...'); - console.log(''); - - // Check if the received apiKey matches either - console.log('๐Ÿ” Value comparison:'); - console.log('API Key === correct UUID:', apiKey === correctClientId); - console.log('API Key === incorrect value:', apiKey === incorrectValue); - console.log('API Key starts with incorrect value:', apiKey.startsWith(incorrectValue.substring(0, 20))); - console.log(''); - - // Check character by character for the first 20 characters - console.log('๐Ÿ”ค Character-by-character comparison (first 20):'); - for (let i = 0; i < Math.min(20, apiKey.length); i++) { - const char = apiKey[i]; - const code = char.charCodeAt(0); - console.log(`Position ${i}: '${char}' (code: ${code})`); - } - - return { - received_api_key: apiKey, - expected_api_key: correctClientId, - matches_expected: apiKey === correctClientId, - api_key_length: apiKey.length, - api_secret_length: apiSecret.length - }; -} \ No newline at end of file diff --git a/lib/test-auth.ts b/lib/test-auth.ts deleted file mode 100644 index d9a82e6..0000000 --- a/lib/test-auth.ts +++ /dev/null @@ -1,221 +0,0 @@ -import axios from 'axios'; - -export async function testAcquiaAuth(apiKey: string, apiSecret: string) { - console.log('๐Ÿงช DETAILED ACQUIA AUTH DEBUG...'); - - // Comprehensive credential validation - console.log('\n๐Ÿ” CREDENTIAL ANALYSIS:'); - console.log('Raw API Key:', JSON.stringify(apiKey)); - console.log('Raw API Secret:', JSON.stringify(apiSecret)); - console.log('API Key length:', apiKey.length); - console.log('API Secret length:', apiSecret.length); - - // Check for hidden characters - const apiKeyBytes = [...apiKey].map(c => c.charCodeAt(0)); - const apiSecretBytes = [...apiSecret].map(c => c.charCodeAt(0)); - console.log('API Key char codes:', apiKeyBytes); - console.log('API Secret char codes (first 20):', apiSecretBytes.slice(0, 20)); - - // Clean the credentials - const cleanApiKey = apiKey.trim().replace(/\s/g, ''); - const cleanApiSecret = apiSecret.trim().replace(/\s/g, ''); - - console.log('\n๐Ÿงน CLEANED CREDENTIALS:'); - console.log('Cleaned API Key:', cleanApiKey); - console.log('Cleaned API Secret (first 20):', cleanApiSecret.substring(0, 20) + '...'); - console.log('API Key UUID format?:', /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cleanApiKey)); - console.log('API Secret base64-like?:', /^[A-Za-z0-9+/]+=*$/.test(cleanApiSecret)); - - // Test the specific endpoint that's most likely to work - const endpoint = 'https://accounts.acquia.com/api/auth/oauth/token'; - - console.log(`\n๐ŸŽฏ TESTING PRIMARY ENDPOINT: ${endpoint}`); - - // Method 1: Form parameters with extensive debugging - console.log('\n๐Ÿ“ Method 1: Form Parameters (Enhanced Debug)'); - try { - const formData = new URLSearchParams(); - formData.append('grant_type', 'client_credentials'); - formData.append('client_id', cleanApiKey); - formData.append('client_secret', cleanApiSecret); - - const requestBody = formData.toString(); - console.log('๐Ÿ“ค Exact request body:', requestBody); - console.log('๐Ÿ“ค Request body length:', requestBody.length); - - const requestConfig = { - method: 'POST' as const, - url: endpoint, - data: requestBody, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json', - 'User-Agent': 'Acquia-Test/1.0', - 'Cache-Control': 'no-cache' - }, - timeout: 30000, - validateStatus: () => true, - }; - - console.log('๐Ÿ“ค Full request config:', JSON.stringify(requestConfig, null, 2)); - - const response = await axios(requestConfig); - - console.log('๐Ÿ“ฅ Complete response:'); - console.log(' Status:', response.status); - console.log(' Status Text:', response.statusText); - console.log(' Headers:', JSON.stringify(response.headers, null, 2)); - console.log(' Data:', JSON.stringify(response.data, null, 2)); - - if (response.status === 200 && response.data?.access_token) { - console.log('โœ… SUCCESS!'); - return { - success: true, - method: 'form_params', - token: response.data.access_token, - data: response.data - }; - } - - // Check for specific error messages - if (response.data?.error) { - console.log('๐Ÿ” API Error Details:', response.data.error); - if (response.data.error_description) { - console.log('๐Ÿ” Error Description:', response.data.error_description); - } - } - - } catch (error) { - console.log('โŒ Form method error:', error); - if (axios.isAxiosError(error)) { - console.log('๐Ÿ” Axios error details:'); - console.log(' Response status:', error.response?.status); - console.log(' Response data:', JSON.stringify(error.response?.data, null, 2)); - console.log(' Request config:', JSON.stringify(error.config, null, 2)); - } - } - - // Method 2: Try with different client_id formats - console.log('\n๐Ÿ”„ Method 2: Testing Different Client ID Formats'); - const clientIdVariations = [ - cleanApiKey, - cleanApiKey.toLowerCase(), - cleanApiKey.toUpperCase(), - cleanApiKey.replace(/-/g, ''), // Remove dashes - ]; - - for (let i = 0; i < clientIdVariations.length; i++) { - const clientId = clientIdVariations[i]; - console.log(`\n๐Ÿ”ง Variation ${i + 1}: ${clientId}`); - - try { - const formData = new URLSearchParams(); - formData.append('grant_type', 'client_credentials'); - formData.append('client_id', clientId); - formData.append('client_secret', cleanApiSecret); - - const response = await axios({ - method: 'POST', - url: endpoint, - data: formData.toString(), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json', - 'User-Agent': 'Acquia-Test/1.0' - }, - timeout: 30000, - validateStatus: () => true, - }); - - console.log(`๐Ÿ“ฅ Variation ${i + 1} response:`, { - status: response.status, - data: response.data - }); - - if (response.status === 200 && response.data?.access_token) { - console.log(`โœ… SUCCESS with variation ${i + 1}!`); - return { - success: true, - method: 'form_params_variation', - variation: i + 1, - client_id_used: clientId, - token: response.data.access_token, - data: response.data - }; - } - } catch (error) { - console.log(`โŒ Variation ${i + 1} error:`, error instanceof Error ? error.message : String(error)); - } - } - - // Method 3: Basic Auth with various credential formats - console.log('\n๐Ÿ” Method 3: Basic Auth Variations'); - for (let i = 0; i < clientIdVariations.length; i++) { - const clientId = clientIdVariations[i]; - console.log(`\n๐Ÿ”ง Basic Auth Variation ${i + 1}: ${clientId}`); - - try { - const credentials = Buffer.from(`${clientId}:${cleanApiSecret}`).toString('base64'); - - const response = await axios({ - method: 'POST', - url: endpoint, - data: 'grant_type=client_credentials', - headers: { - 'Authorization': `Basic ${credentials}`, - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json', - 'User-Agent': 'Acquia-Test/1.0' - }, - timeout: 30000, - validateStatus: () => true, - }); - - console.log(`๐Ÿ“ฅ Basic Auth Variation ${i + 1} response:`, { - status: response.status, - data: response.data - }); - - if (response.status === 200 && response.data?.access_token) { - console.log(`โœ… SUCCESS with Basic Auth variation ${i + 1}!`); - return { - success: true, - method: 'basic_auth_variation', - variation: i + 1, - client_id_used: clientId, - token: response.data.access_token, - data: response.data - }; -} - } catch (error) { - console.log(`โŒ Basic Auth Variation ${i + 1} error:`, error instanceof Error ? error.message : String(error)); - } - } - - // Method 4: Test credential generation - console.log('\n๐Ÿงช Method 4: Credential Generation Test'); - console.log('Testing if we can reproduce your working curl command...'); - - // This should exactly match what curl would send - const curlEquivalent = { - client_id: cleanApiKey, - client_secret: cleanApiSecret, - base64: Buffer.from(`${cleanApiKey}:${cleanApiSecret}`).toString('base64') - }; - - console.log('๐Ÿ” Curl equivalent data:'); - console.log(' client_id:', curlEquivalent.client_id); - console.log(' client_secret (first 10):', curlEquivalent.client_secret.substring(0, 10) + '...'); - console.log(' base64 (first 20):', curlEquivalent.base64.substring(0, 20) + '...'); - - return { - success: false, - error: 'All methods failed', - debug_info: { - original_api_key: apiKey, - cleaned_api_key: cleanApiKey, - api_secret_length: cleanApiSecret.length, - tested_variations: clientIdVariations - } - }; -}