UNPKG

api-scout

Version:

๐Ÿ” Automatically scout, discover and generate beautiful interactive API documentation from your codebase. Supports Express.js, NestJS, FastAPI, Spring Boot with interactive testing and security analysis.

958 lines (821 loc) โ€ข 31.2 kB
class InteractiveTester { constructor() { this.authTokens = new Map(); this.requestHistory = []; this.environments = { development: 'http://localhost:3000', staging: 'https://staging-api.example.com', production: 'https://api.example.com' }; this.currentEnvironment = 'development'; } generateTesterHTML() { return ` <!-- Interactive API Tester --> <div id="api-tester-modal" class="tester-modal" style="display: none;"> <div class="tester-modal-content"> <div class="tester-header"> <h2>๐Ÿงช API Tester</h2> <div class="tester-controls"> <select id="environment-selector" class="env-select"> <option value="development">Development</option> <option value="staging">Staging</option> <option value="production">Production</option> </select> <button class="close-tester" onclick="closeTester()">&times;</button> </div> </div> <div class="tester-body"> <div class="tester-tabs"> <button class="tab-btn active" onclick="showTab('request')">Request</button> <button class="tab-btn" onclick="showTab('auth')">Auth</button> <button class="tab-btn" onclick="showTab('headers')">Headers</button> <button class="tab-btn" onclick="showTab('history')">History</button> </div> <!-- Request Tab --> <div id="request-tab" class="tab-content active"> <div class="request-builder"> <div class="request-line"> <select id="method-select" class="method-select"> <option value="GET">GET</option> <option value="POST">POST</option> <option value="PUT">PUT</option> <option value="DELETE">DELETE</option> <option value="PATCH">PATCH</option> </select> <input type="text" id="url-input" class="url-input" placeholder="Enter endpoint URL..."> <button id="send-request" class="send-btn" onclick="sendRequest()">Send</button> </div> <div class="request-params"> <h4>Parameters</h4> <div id="params-container"> <div class="param-row"> <input type="text" placeholder="Key" class="param-key"> <input type="text" placeholder="Value" class="param-value"> <select class="param-type"> <option value="query">Query</option> <option value="path">Path</option> </select> <button onclick="removeParam(this)" class="remove-param">โˆ’</button> </div> </div> <button onclick="addParam()" class="add-param">+ Add Parameter</button> </div> <div class="request-body"> <h4>Request Body</h4> <div class="body-type-selector"> <label><input type="radio" name="bodyType" value="json" checked> JSON</label> <label><input type="radio" name="bodyType" value="form"> Form Data</label> <label><input type="radio" name="bodyType" value="raw"> Raw</label> </div> <textarea id="request-body" class="body-editor" placeholder="Enter request body..."></textarea> </div> </div> </div> <!-- Auth Tab --> <div id="auth-tab" class="tab-content"> <div class="auth-config"> <h4>Authentication</h4> <select id="auth-type" onchange="showAuthConfig()"> <option value="none">No Auth</option> <option value="bearer">Bearer Token</option> <option value="apikey">API Key</option> <option value="basic">Basic Auth</option> </select> <div id="auth-config-bearer" class="auth-config-section" style="display: none;"> <label>Bearer Token:</label> <input type="text" id="bearer-token" placeholder="Enter your bearer token..."> </div> <div id="auth-config-apikey" class="auth-config-section" style="display: none;"> <label>API Key Name:</label> <input type="text" id="apikey-name" placeholder="x-api-key"> <label>API Key Value:</label> <input type="text" id="apikey-value" placeholder="Enter your API key..."> <label>Add to:</label> <select id="apikey-location"> <option value="header">Header</option> <option value="query">Query Parameter</option> </select> </div> <div id="auth-config-basic" class="auth-config-section" style="display: none;"> <label>Username:</label> <input type="text" id="basic-username" placeholder="Username"> <label>Password:</label> <input type="password" id="basic-password" placeholder="Password"> </div> </div> </div> <!-- Headers Tab --> <div id="headers-tab" class="tab-content"> <div class="headers-config"> <h4>Headers</h4> <div id="headers-container"> <div class="header-row"> <input type="text" placeholder="Header Name" class="header-key"> <input type="text" placeholder="Header Value" class="header-value"> <button onclick="removeHeader(this)" class="remove-header">โˆ’</button> </div> </div> <button onclick="addHeader()" class="add-header">+ Add Header</button> </div> </div> <!-- History Tab --> <div id="history-tab" class="tab-content"> <div class="request-history"> <h4>Request History</h4> <div id="history-list"> <!-- History items will be populated here --> </div> <button onclick="clearHistory()" class="clear-history">Clear History</button> </div> </div> </div> <div class="tester-response"> <div class="response-header"> <h4>Response</h4> <div class="response-status" id="response-status"></div> </div> <div class="response-tabs"> <button class="response-tab active" onclick="showResponseTab('body')">Body</button> <button class="response-tab" onclick="showResponseTab('headers')">Headers</button> <button class="response-tab" onclick="showResponseTab('raw')">Raw</button> </div> <div class="response-content"> <div id="response-body" class="response-section active"> <pre><code id="response-json"></code></pre> </div> <div id="response-headers" class="response-section"> <pre><code id="response-headers-content"></code></pre> </div> <div id="response-raw" class="response-section"> <pre><code id="response-raw-content"></code></pre> </div> </div> </div> </div> </div> <style> .tester-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; align-items: center; justify-content: center; } .tester-modal-content { background: white; border-radius: 12px; width: 90%; max-width: 1200px; height: 80%; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } .tester-header { padding: 1.5rem; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px 12px 0 0; } .tester-controls { display: flex; align-items: center; gap: 1rem; } .env-select { padding: 0.5rem; border: 1px solid rgba(255,255,255,0.3); border-radius: 6px; background: rgba(255,255,255,0.1); color: white; } .close-tester { background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer; padding: 0.5rem; border-radius: 4px; } .close-tester:hover { background: rgba(255,255,255,0.1); } .tester-body { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .tester-tabs { display: flex; border-bottom: 1px solid #e0e0e0; background: #f8f9fa; } .tab-btn { padding: 1rem 1.5rem; border: none; background: none; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.2s; } .tab-btn.active { border-bottom-color: #667eea; background: white; color: #667eea; font-weight: bold; } .tab-content { display: none; flex: 1; padding: 1.5rem; overflow-y: auto; } .tab-content.active { display: block; } .request-line { display: flex; gap: 1rem; margin-bottom: 1.5rem; align-items: center; } .method-select { padding: 0.75rem; border: 2px solid #e0e0e0; border-radius: 6px; font-weight: bold; min-width: 100px; } .url-input { flex: 1; padding: 0.75rem; border: 2px solid #e0e0e0; border-radius: 6px; font-family: monospace; } .send-btn { padding: 0.75rem 2rem; background: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; transition: background 0.2s; } .send-btn:hover { background: #45a049; } .send-btn:disabled { background: #ccc; cursor: not-allowed; } .param-row, .header-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; } .param-key, .param-value, .param-type, .header-key, .header-value { padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; } .param-key, .header-key { flex: 1; } .param-value, .header-value { flex: 2; } .param-type { flex: 0.8; } .remove-param, .remove-header { background: #f44336; color: white; border: none; border-radius: 4px; width: 30px; height: 30px; cursor: pointer; } .add-param, .add-header { background: #2196F3; color: white; border: none; border-radius: 4px; padding: 0.5rem 1rem; cursor: pointer; margin-top: 0.5rem; } .body-type-selector { margin-bottom: 1rem; } .body-type-selector label { margin-right: 1rem; cursor: pointer; } .body-editor { width: 100%; height: 150px; padding: 1rem; border: 1px solid #ddd; border-radius: 6px; font-family: monospace; resize: vertical; } .auth-config-section { margin-top: 1rem; padding: 1rem; border: 1px solid #e0e0e0; border-radius: 6px; background: #f9f9f9; } .auth-config-section label { display: block; margin-bottom: 0.5rem; font-weight: bold; } .auth-config-section input, .auth-config-section select { width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 1rem; } .tester-response { height: 40%; border-top: 1px solid #e0e0e0; display: flex; flex-direction: column; } .response-header { padding: 1rem 1.5rem; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; background: #f8f9fa; } .response-status { font-weight: bold; padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.875rem; } .response-status.success { background: #d4edda; color: #155724; } .response-status.error { background: #f8d7da; color: #721c24; } .response-tabs { display: flex; background: #f8f9fa; } .response-tab { padding: 0.75rem 1rem; border: none; background: none; cursor: pointer; border-bottom: 2px solid transparent; } .response-tab.active { border-bottom-color: #667eea; background: white; color: #667eea; font-weight: bold; } .response-content { flex: 1; position: relative; overflow: hidden; } .response-section { display: none; height: 100%; overflow-y: auto; padding: 1rem; } .response-section.active { display: block; } .response-section pre { margin: 0; background: #f8f9fa; padding: 1rem; border-radius: 6px; border: 1px solid #e0e0e0; overflow-x: auto; } .response-section code { font-family: 'Courier New', monospace; font-size: 0.875rem; line-height: 1.4; } .test-btn { background: #FF9800; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.875rem; margin-left: 1rem; transition: background 0.2s; } .test-btn:hover { background: #F57C00; } .loading { opacity: 0.7; pointer-events: none; } .loading::after { content: ''; position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; border: 2px solid #ccc; border-top-color: #667eea; border-radius: 50%; animation: spin 1s linear infinite; transform: translate(-50%, -50%); } @keyframes spin { to { transform: translate(-50%, -50%) rotate(360deg); } } .history-item { padding: 0.75rem; border: 1px solid #e0e0e0; border-radius: 6px; margin-bottom: 0.5rem; cursor: pointer; transition: background 0.2s; } .history-item:hover { background: #f8f9fa; } .history-method { font-weight: bold; margin-right: 0.5rem; } .history-url { font-family: monospace; color: #666; } .history-time { font-size: 0.75rem; color: #999; float: right; } .clear-history { background: #f44336; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; margin-top: 1rem; } </style>`; } generateTesterJS() { return ` // Interactive API Tester JavaScript class APITester { constructor() { this.environments = { development: 'http://localhost:3000', staging: 'https://staging-api.example.com', production: 'https://api.example.com' }; this.currentEnvironment = 'development'; this.requestHistory = JSON.parse(localStorage.getItem('apiTesterHistory') || '[]'); this.authConfig = JSON.parse(localStorage.getItem('apiTesterAuth') || '{}'); this.loadHistory(); this.loadAuthConfig(); } openTester(method = 'GET', path = '', params = []) { document.getElementById('api-tester-modal').style.display = 'flex'; document.getElementById('method-select').value = method; document.getElementById('url-input').value = path; // Populate parameters this.clearParams(); params.forEach(param => { this.addParam(param.name, param.type); }); // Auto-select appropriate body type for POST/PUT/PATCH if (['POST', 'PUT', 'PATCH'].includes(method)) { document.querySelector('input[name="bodyType"][value="json"]').checked = true; document.getElementById('request-body').value = JSON.stringify({ // Sample based on detected parameters }, null, 2); } } async sendRequest() { const method = document.getElementById('method-select').value; const url = this.buildURL(); const headers = this.buildHeaders(); const body = this.buildBody(); document.getElementById('send-request').disabled = true; document.getElementById('send-request').textContent = 'Sending...'; const startTime = Date.now(); try { const response = await fetch(url, { method, headers, body: ['GET', 'HEAD'].includes(method) ? undefined : body }); const responseTime = Date.now() - startTime; await this.displayResponse(response, responseTime); this.saveToHistory(method, url, response.status, responseTime); } catch (error) { this.displayError(error); } finally { document.getElementById('send-request').disabled = false; document.getElementById('send-request').textContent = 'Send'; } } buildURL() { const baseURL = this.environments[this.currentEnvironment]; const path = document.getElementById('url-input').value; const url = new URL(path.startsWith('/') ? baseURL + path : path); // Add query parameters document.querySelectorAll('#params-container .param-row').forEach(row => { const key = row.querySelector('.param-key').value; const value = row.querySelector('.param-value').value; const type = row.querySelector('.param-type').value; if (key && value && type === 'query') { url.searchParams.append(key, value); } }); return url.toString(); } buildHeaders() { const headers = { 'Content-Type': 'application/json' }; // Add custom headers document.querySelectorAll('#headers-container .header-row').forEach(row => { const key = row.querySelector('.header-key').value; const value = row.querySelector('.header-value').value; if (key && value) { headers[key] = value; } }); // Add authentication headers const authType = document.getElementById('auth-type').value; if (authType === 'bearer') { const token = document.getElementById('bearer-token').value; if (token) { headers['Authorization'] = \`Bearer \${token}\`; } } else if (authType === 'apikey') { const keyName = document.getElementById('apikey-name').value || 'x-api-key'; const keyValue = document.getElementById('apikey-value').value; const location = document.getElementById('apikey-location').value; if (keyValue && location === 'header') { headers[keyName] = keyValue; } } else if (authType === 'basic') { const username = document.getElementById('basic-username').value; const password = document.getElementById('basic-password').value; if (username && password) { headers['Authorization'] = \`Basic \${btoa(username + ':' + password)}\`; } } return headers; } buildBody() { const method = document.getElementById('method-select').value; if (['GET', 'HEAD'].includes(method)) return null; const bodyType = document.querySelector('input[name="bodyType"]:checked').value; const bodyContent = document.getElementById('request-body').value; if (bodyType === 'json' && bodyContent) { try { JSON.parse(bodyContent); // Validate JSON return bodyContent; } catch (e) { alert('Invalid JSON in request body'); throw e; } } return bodyContent; } async displayResponse(response, responseTime) { const statusElement = document.getElementById('response-status'); statusElement.textContent = \`\${response.status} \${response.statusText} (\${responseTime}ms)\`; statusElement.className = \`response-status \${response.ok ? 'success' : 'error'}\`; // Response body const responseText = await response.text(); let formattedBody = responseText; try { const jsonData = JSON.parse(responseText); formattedBody = JSON.stringify(jsonData, null, 2); } catch (e) { // Not JSON, keep as text } document.getElementById('response-json').textContent = formattedBody; // Response headers const headersText = Array.from(response.headers.entries()) .map(([key, value]) => \`\${key}: \${value}\`) .join('\\n'); document.getElementById('response-headers-content').textContent = headersText; // Raw response document.getElementById('response-raw-content').textContent = \`HTTP/1.1 \${response.status} \${response.statusText}\\n\${headersText}\\n\\n\${responseText}\`; } displayError(error) { const statusElement = document.getElementById('response-status'); statusElement.textContent = \`Error: \${error.message}\`; statusElement.className = 'response-status error'; document.getElementById('response-json').textContent = error.toString(); document.getElementById('response-headers-content').textContent = ''; document.getElementById('response-raw-content').textContent = error.toString(); } saveToHistory(method, url, status, responseTime) { const historyItem = { id: Date.now(), method, url, status, responseTime, timestamp: new Date().toISOString() }; this.requestHistory.unshift(historyItem); this.requestHistory = this.requestHistory.slice(0, 50); // Keep last 50 localStorage.setItem('apiTesterHistory', JSON.stringify(this.requestHistory)); this.loadHistory(); } loadHistory() { const historyList = document.getElementById('history-list'); historyList.innerHTML = ''; this.requestHistory.forEach(item => { const historyItem = document.createElement('div'); historyItem.className = 'history-item'; historyItem.onclick = () => this.loadFromHistory(item); historyItem.innerHTML = \` <div class="history-time">\${new Date(item.timestamp).toLocaleString()}</div> <span class="history-method \${item.method.toLowerCase()}">\${item.method}</span> <span class="history-url">\${item.url}</span> <span class="response-status \${item.status < 400 ? 'success' : 'error'}">\${item.status}</span> \`; historyList.appendChild(historyItem); }); } loadFromHistory(item) { document.getElementById('method-select').value = item.method; document.getElementById('url-input').value = item.url; this.showTab('request'); } clearHistory() { this.requestHistory = []; localStorage.removeItem('apiTesterHistory'); this.loadHistory(); } clearParams() { const container = document.getElementById('params-container'); container.innerHTML = \` <div class="param-row"> <input type="text" placeholder="Key" class="param-key"> <input type="text" placeholder="Value" class="param-value"> <select class="param-type"> <option value="query">Query</option> <option value="path">Path</option> </select> <button onclick="removeParam(this)" class="remove-param">โˆ’</button> </div> \`; } addParam(key = '', type = 'query') { const container = document.getElementById('params-container'); const paramRow = document.createElement('div'); paramRow.className = 'param-row'; paramRow.innerHTML = \` <input type="text" placeholder="Key" class="param-key" value="\${key}"> <input type="text" placeholder="Value" class="param-value"> <select class="param-type"> <option value="query" \${type === 'query' ? 'selected' : ''}>Query</option> <option value="path" \${type === 'path' ? 'selected' : ''}>Path</option> </select> <button onclick="removeParam(this)" class="remove-param">โˆ’</button> \`; container.appendChild(paramRow); } loadAuthConfig() { if (this.authConfig.type) { document.getElementById('auth-type').value = this.authConfig.type; this.showAuthConfig(); if (this.authConfig.type === 'bearer' && this.authConfig.token) { document.getElementById('bearer-token').value = this.authConfig.token; } } } saveAuthConfig() { const authType = document.getElementById('auth-type').value; const config = { type: authType }; if (authType === 'bearer') { config.token = document.getElementById('bearer-token').value; } this.authConfig = config; localStorage.setItem('apiTesterAuth', JSON.stringify(config)); } } // Global functions window.apiTester = new APITester(); function openTester(method, path, params = []) { window.apiTester.openTester(method, path, params); } function closeTester() { document.getElementById('api-tester-modal').style.display = 'none'; } function showTab(tabName) { document.querySelectorAll('.tab-content').forEach(tab => { tab.classList.remove('active'); }); document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active'); }); document.getElementById(tabName + '-tab').classList.add('active'); event.target.classList.add('active'); } function showResponseTab(tabName) { document.querySelectorAll('.response-section').forEach(section => { section.classList.remove('active'); }); document.querySelectorAll('.response-tab').forEach(btn => { btn.classList.remove('active'); }); document.getElementById('response-' + tabName).classList.add('active'); event.target.classList.add('active'); } function showAuthConfig() { const authType = document.getElementById('auth-type').value; document.querySelectorAll('.auth-config-section').forEach(section => { section.style.display = 'none'; }); if (authType !== 'none') { const section = document.getElementById('auth-config-' + authType); if (section) { section.style.display = 'block'; } } window.apiTester.saveAuthConfig(); } function addParam() { window.apiTester.addParam(); } function removeParam(button) { button.closest('.param-row').remove(); } function addHeader() { const container = document.getElementById('headers-container'); const headerRow = document.createElement('div'); headerRow.className = 'header-row'; headerRow.innerHTML = \` <input type="text" placeholder="Header Name" class="header-key"> <input type="text" placeholder="Header Value" class="header-value"> <button onclick="removeHeader(this)" class="remove-header">โˆ’</button> \`; container.appendChild(headerRow); } function removeHeader(button) { button.closest('.header-row').remove(); } function sendRequest() { window.apiTester.sendRequest(); } function clearHistory() { window.apiTester.clearHistory(); } // Environment selector document.addEventListener('DOMContentLoaded', function() { const envSelector = document.getElementById('environment-selector'); if (envSelector) { envSelector.addEventListener('change', function() { window.apiTester.currentEnvironment = this.value; }); } }); `; } } module.exports = InteractiveTester;