UNPKG

playground-azure

Version:

OAuth2 Azure Playground - Interactive web application for testing Microsoft Graph APIs

692 lines (576 loc) 25.3 kB
// Utility Functions Module // Toast notification system function showToast(message, type = 'info') { const toastHtml = ` <div class="toast align-items-center text-white bg-${type} border-0" role="alert"> <div class="d-flex"> <div class="toast-body"> ${message} </div> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> </div> </div> `; let container = document.querySelector('.toast-container'); if (!container) { container = document.createElement('div'); container.className = 'toast-container position-fixed top-0 end-0 p-3'; document.body.appendChild(container); } container.insertAdjacentHTML('beforeend', toastHtml); const toast = new bootstrap.Toast(container.lastElementChild); toast.show(); setTimeout(() => { container.lastElementChild.remove(); }, 5000); } // Token management functions function copyToken(token) { navigator.clipboard.writeText(token).then(() => { showToast('✅ Token copied to clipboard', 'success'); }).catch(err => { showToast('❌ Failed to copy token', 'danger'); }); } function refreshToken(tokenId) { if (!confirm('Refresh token? This will generate a new access token.')) { return; } const btn = event.target.closest('button'); const originalText = btn.innerHTML; btn.innerHTML = '<i class="bi bi-arrow-clockwise spin"></i> Refreshing...'; btn.disabled = true; console.log('🔄 Refreshing token with ID:', tokenId); axios.post(`/api/token/${tokenId}/refresh`) .then(response => { console.log('✅ Refresh response:', response.data); if (response.data.success) { showToast('✅ Token refreshed successfully', 'success'); setTimeout(() => location.reload(), 1500); } else { throw new Error(response.data.error || 'Unknown refresh error'); } }) .catch(error => { console.error('❌ Refresh error:', error); const errorMessage = error.response?.data?.error || error.message; showToast('❌ Refresh failed: ' + errorMessage, 'danger'); // Restore button state on error btn.innerHTML = originalText; btn.disabled = false; }); } function revokeToken(tokenId) { if (!confirm('Revoke this token? This action cannot be undone.')) { return; } const btn = event.target.closest('button'); const originalText = btn.innerHTML; btn.innerHTML = '<i class="bi bi-x-circle"></i> Revoking...'; btn.disabled = true; axios.post(`/api/token/${tokenId}/revoke`) .then(response => { if (response.data.success) { showToast('✅ Token revoked successfully', 'success'); setTimeout(() => location.reload(), 1000); } else { showToast('❌ Failed to revoke: ' + response.data.error, 'danger'); } }) .catch(error => { showToast('❌ Revoke error: ' + error.message, 'danger'); }) .finally(() => { btn.innerHTML = originalText; btn.disabled = false; }); } // URI Parameter Management Functions function detectUriParameters() { const uriInput = document.getElementById('requestUri'); const parametersSection = document.getElementById('uriParametersSection'); const parametersList = document.getElementById('uriParametersList'); if (!uriInput || !parametersSection || !parametersList) return; const uri = uriInput.value; // Find parameters in URI (pattern: {param-name}) const parameterRegex = /\{([^}]+)\}/g; const parameters = []; let match; while ((match = parameterRegex.exec(uri)) !== null) { if (!parameters.includes(match[1])) { parameters.push(match[1]); } } // Clear existing parameters parametersList.innerHTML = ''; if (parameters.length > 0) { // Show parameters section parametersSection.style.display = 'block'; // Create input fields for each parameter parameters.forEach(param => { const paramField = document.createElement('div'); paramField.className = 'uri-parameter-field'; paramField.innerHTML = ` <label class="uri-parameter-label"> <strong>{${param}}</strong> </label> <input type="text" class="uri-parameter-input" data-param="${param}" placeholder="Enter ${param} value" title="Parameter for ${param}"> `; parametersList.appendChild(paramField); }); } else { // Hide parameters section parametersSection.style.display = 'none'; } } function applyUriParameters() { const uriInput = document.getElementById('requestUri'); const parameterInputs = document.querySelectorAll('.uri-parameter-input'); if (!uriInput) return; let uri = uriInput.value; let appliedCount = 0; parameterInputs.forEach(input => { const paramName = input.dataset.param; const paramValue = input.value.trim(); if (paramValue) { // Replace {param-name} with the actual value const oldUri = uri; uri = uri.replace(`{${paramName}}`, encodeURIComponent(paramValue)); if (oldUri !== uri) { appliedCount++; } } }); // Update the URI input uriInput.value = uri; if (appliedCount > 0) { showToast(`✅ Applied ${appliedCount} parameter(s) to URI`, 'success'); } else { showToast('⚠️ No parameters to apply or values are empty', 'warning'); } } function toggleRequestBody() { const httpMethod = document.getElementById('httpMethod').value; const requestBodySection = document.getElementById('requestBodySection'); if (!requestBodySection) return; // Show request body for methods that typically have a body const methodsWithBody = ['POST', 'PATCH', 'PUT']; if (methodsWithBody.includes(httpMethod)) { requestBodySection.style.display = 'block'; } else { requestBodySection.style.display = 'none'; // Clear the request body when hiding const requestBody = document.getElementById('requestBody'); if (requestBody) { requestBody.value = ''; } } } // Query Parameter Management Functions function detectQueryParameters() { const uriInput = document.getElementById('requestUri'); const queryParamBtn = document.getElementById('queryParamBtn'); if (!uriInput || !queryParamBtn) return; const uri = uriInput.value; const hasQueryParams = uri.includes('?'); // Show/hide the edit query parameters button if (hasQueryParams) { queryParamBtn.style.display = 'block'; } else { queryParamBtn.style.display = 'none'; // Hide query editor if no params const querySection = document.getElementById('queryParametersSection'); if (querySection) { querySection.style.display = 'none'; } } } function toggleQueryEditor() { const querySection = document.getElementById('queryParametersSection'); const uriInput = document.getElementById('requestUri'); if (!querySection || !uriInput) return; const isVisible = querySection.style.display === 'block'; if (isVisible) { querySection.style.display = 'none'; } else { querySection.style.display = 'block'; parseQueryParametersFromUri(); } } function parseQueryParametersFromUri() { const uriInput = document.getElementById('requestUri'); const parametersList = document.getElementById('queryParametersList'); if (!uriInput || !parametersList) return; const uri = uriInput.value; const queryStartIndex = uri.indexOf('?'); if (queryStartIndex === -1) return; // Clear existing parameters parametersList.innerHTML = ''; const queryString = uri.substring(queryStartIndex + 1); const params = new URLSearchParams(queryString); // Create input fields for existing parameters params.forEach((value, key) => { addQueryParameterField(key, value); }); // Add empty field for new parameter if (params.size === 0) { addQueryParameterField('', ''); } } function addQueryParameter() { addQueryParameterField('', ''); } function addQueryParameterField(key = '', value = '') { const parametersList = document.getElementById('queryParametersList'); if (!parametersList) return; const paramField = document.createElement('div'); paramField.className = 'query-parameter-field'; paramField.innerHTML = ` <input type="text" class="query-param-key" placeholder="Parameter name" value="${key}" title="Parameter name (e.g., $top, $filter)"> <span style="color: #6c757d; font-size: 12px;">=</span> <input type="text" class="query-param-value" placeholder="Parameter value" value="${value}" title="Parameter value"> <button type="button" class="query-param-remove" onclick="removeQueryParameter(this)" title="Remove parameter"> <i class="bi bi-x"></i> </button> `; parametersList.appendChild(paramField); } function removeQueryParameter(button) { const paramField = button.closest('.query-parameter-field'); if (paramField) { paramField.remove(); } } function applyQueryParameters() { const uriInput = document.getElementById('requestUri'); const parametersList = document.getElementById('queryParametersList'); if (!uriInput || !parametersList) return; let uri = uriInput.value; // Remove existing query parameters const queryStartIndex = uri.indexOf('?'); if (queryStartIndex !== -1) { uri = uri.substring(0, queryStartIndex); } // Collect all parameter fields const paramFields = parametersList.querySelectorAll('.query-parameter-field'); const params = new URLSearchParams(); let validParamCount = 0; paramFields.forEach(field => { const keyInput = field.querySelector('.query-param-key'); const valueInput = field.querySelector('.query-param-value'); if (keyInput && valueInput) { const key = keyInput.value.trim(); const value = valueInput.value.trim(); if (key) { params.append(key, value); validParamCount++; } } }); // Apply parameters to URI if (validParamCount > 0) { uri += '?' + params.toString(); showToast(`✅ Applied ${validParamCount} query parameter(s)`, 'success'); } else { showToast('⚠️ No valid query parameters to apply', 'warning'); } // Update the URI input uriInput.value = uri; } // Configuration management function loadSavedConfiguration() { console.log('📋 === DEBUGGING SCOPE LOADING ==='); const customScopesInput = document.getElementById('customScopes'); console.log('📋 Loading saved configuration...'); console.log('📋 customScopesInput element:', customScopesInput); console.log('📋 Custom scopes input value:', customScopesInput ? `"${customScopesInput.value}"` : 'not found'); console.log('📋 Value length:', customScopesInput ? customScopesInput.value.length : 0); console.log('📋 Value trimmed:', customScopesInput ? `"${customScopesInput.value.trim()}"` : 'n/a'); if (customScopesInput && customScopesInput.value.trim()) { const savedScopes = customScopesInput.value.split(' ').filter(s => s.trim()); console.log('🔍 Found saved scopes:', savedScopes); // Map of scopes to checkbox IDs const scopeToCheckboxMap = { 'https://graph.microsoft.com/User.Read': 'user-read', 'https://graph.microsoft.com/Calendars.Read': 'calendars-read', 'https://graph.microsoft.com/Calendars.ReadWrite': 'calendars-readwrite', 'https://graph.microsoft.com/Contacts.Read': 'contacts-read', 'https://graph.microsoft.com/People.Read': 'people-read', 'https://graph.microsoft.com/Files.Read': 'files-read', 'https://graph.microsoft.com/Files.Read.All': 'files-read-all', 'https://graph.microsoft.com/Sites.Read.All': 'sites-read-all', 'https://graph.microsoft.com/Chat.Read': 'chat-read', 'https://graph.microsoft.com/Chat.ReadWrite': 'chat-readwrite' }; let checkedCount = 0; // Check the appropriate checkboxes savedScopes.forEach(scope => { console.log('🔄 Processing scope:', scope); const checkboxId = scopeToCheckboxMap[scope]; if (checkboxId) { const checkbox = document.getElementById(checkboxId); if (checkbox) { console.log('✅ Checking checkbox:', checkboxId); checkbox.checked = true; checkedCount++; // Also check the parent group checkbox if all items in group are checked const groupCheckbox = checkbox.closest('.scope-group')?.querySelector('.scope-group-checkbox'); if (groupCheckbox) { const allItemsInGroup = groupCheckbox.closest('.scope-group').querySelectorAll('.scope-checkbox'); const checkedItemsInGroup = groupCheckbox.closest('.scope-group').querySelectorAll('.scope-checkbox:checked'); if (allItemsInGroup.length === checkedItemsInGroup.length) { groupCheckbox.checked = true; } } } else { console.log('❌ Checkbox not found for:', checkboxId); } } else if (scope === 'offline_access') { // Check offline_access checkbox const offlineAccessCheckbox = document.getElementById('offline-access'); if (offlineAccessCheckbox) { console.log('✅ Checking offline_access checkbox'); offlineAccessCheckbox.checked = true; checkedCount++; } else { console.log('❌ offline_access checkbox not found'); } } else { console.log('⚠️ No checkbox mapping found for scope:', scope); } }); console.log('📊 Total checkboxes checked:', checkedCount, 'out of', savedScopes.length, 'saved scopes'); } else { console.log('📋 No saved scopes found or empty value'); } } // Token expiration functions function updateTokenExpiration() { const tokenExpirationEl = document.getElementById('tokenExpiration'); const expirationTextEl = document.getElementById('expirationText'); if (!tokenExpirationEl || !expirationTextEl) return; const expiresAt = tokenExpirationEl.getAttribute('data-expires'); if (!expiresAt) return; const expirationTime = new Date(expiresAt); const now = new Date(); const diffMs = expirationTime - now; if (diffMs <= 0) { expirationTextEl.textContent = 'Token expired'; expirationTextEl.className = 'text-danger'; return; } const diffMinutes = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMinutes / 60); const remainingMinutes = diffMinutes % 60; let expirationText; if (diffHours > 0) { expirationText = `Expires in ${diffHours}h ${remainingMinutes}m`; } else if (diffMinutes > 0) { expirationText = `Expires in ${diffMinutes} minutes`; } else { const diffSeconds = Math.floor(diffMs / 1000); expirationText = `Expires in ${diffSeconds} seconds`; } expirationTextEl.textContent = expirationText; expirationTextEl.className = diffMinutes < 5 ? 'text-warning' : 'text-success'; } function startTokenExpirationTimer() { updateTokenExpiration(); // Update every 30 seconds setInterval(updateTokenExpiration, 30000); } // UI Toggle Functions function toggleStep(stepNumber) { const stepSection = document.querySelector(`#step${stepNumber}`); if (!stepSection) return; const stepHeader = stepSection.querySelector('.step-header'); const stepContent = stepSection.querySelector('.step-content'); const toggle = stepHeader.querySelector('.step-toggle'); // Check if step is disabled if (stepHeader.classList.contains('disabled')) { return; } const isCurrentlyHidden = stepContent.style.display === 'none' || window.getComputedStyle(stepContent).display === 'none'; // Close all other steps first (accordion behavior) const allSteps = document.querySelectorAll('.step-section'); allSteps.forEach(step => { if (step.id !== `step${stepNumber}`) { const content = step.querySelector('.step-content'); const stepToggle = step.querySelector('.step-toggle'); if (content && stepToggle) { content.style.display = 'none'; stepToggle.textContent = '▶'; } } }); // Toggle the clicked step if (isCurrentlyHidden) { stepContent.style.display = 'block'; toggle.textContent = '▼'; } else { stepContent.style.display = 'none'; toggle.textContent = '▶'; } } // Contact and Donate Functions function contactDeveloper() { window.open('mailto:nurza.cool@gmail.com?subject=Azure OAuth2 Playground - Contact&body=Hello,%0D%0A%0D%0AI am contacting you regarding the Azure OAuth2 Playground application.%0D%0A%0D%0AMessage:%0D%0A', '_blank'); } function donatePaypal() { // PayPal donation link - opens PayPal send money page window.open('https://paypal.me/nurzazin', '_blank'); } // JSON Viewer utilities function addJsonCopyButtons(containerId = '#jsonViewer') { // Add copy buttons to keys in the JSON viewer $(containerId).find('.json-key').each(function() { const $elem = $(this); // Skip if already has a copy button if ($elem.next('.copy-btn-key').length > 0) { return; } const keyText = $elem.text().replace(/['"]/g, ''); const $copyBtn = $('<button class="btn btn-link btn-sm copy-btn-key p-0 ms-1" title="Copy key" style="font-size: 0.8rem; opacity: 0.7; color: #007bff; background: rgba(0,123,255,0.1); border-radius: 3px; padding: 1px 3px!important;">'); $copyBtn.html('<i class="bi bi-clipboard"></i>'); $copyBtn.hover( function() { $(this).css({ 'opacity': '1', 'background': 'rgba(0,123,255,0.2)', 'transform': 'scale(1.1)' }); }, function() { $(this).css({ 'opacity': '0.7', 'background': 'rgba(0,123,255,0.1)', 'transform': 'scale(1.0)' }); } ); $copyBtn.on('click', function(e) { e.stopPropagation(); navigator.clipboard.writeText(keyText).then(() => { showToast(`✅ Copied key: ${keyText}`, 'success'); }).catch(err => { showToast('❌ Failed to copy key', 'danger'); }); }); $elem.after($copyBtn); }); // Add copy buttons to string values $(containerId).find('.json-value').each(function() { const $elem = $(this); if (!$elem.hasClass('json-string') || $elem.next('.copy-btn-value').length > 0) { return; } const valueText = $elem.text().replace(/['"]/g, ''); // Skip empty or very short values if (!valueText || valueText.length < 3) { return; } const $copyBtn = $('<button class="btn btn-link btn-sm copy-btn-value p-0 ms-1" title="Copy value" style="font-size: 0.8rem; opacity: 0.5; color: #28a745; background: rgba(40,167,69,0.1); border-radius: 3px; padding: 1px 3px!important;">'); $copyBtn.html('<i class="bi bi-copy"></i>'); $copyBtn.hover( function() { $(this).css({ 'opacity': '1', 'background': 'rgba(40,167,69,0.2)', 'color': '#1e7e34' }); }, function() { $(this).css({ 'opacity': '0.5', 'background': 'rgba(40,167,69,0.1)', 'color': '#28a745' }); } ); $copyBtn.on('click', function(e) { e.stopPropagation(); navigator.clipboard.writeText(valueText).then(() => { showToast(`✅ Copied: ${valueText.length > 30 ? valueText.substring(0, 30) + '...' : valueText}`, 'success'); }).catch(err => { showToast('❌ Failed to copy value', 'danger'); }); }); $elem.after($copyBtn); }); } // Display original token response after authorization function displayOriginalTokenResponse(response) { const requestResponseArea = document.getElementById('requestResponse'); if (!requestResponseArea || !response) return; // Create response display HTML const responseHTML = ` <div class="api-response"> <div class="response-header"> <div class="d-flex justify-content-between align-items-center mb-2"> <h6 class="mb-0 text-success"> <i class="bi bi-check-circle-fill"></i> Authorization Success </h6> <div class="response-actions"> <button class="btn btn-sm btn-outline-primary me-1" onclick="copyFullResponse()"> <i class="bi bi-clipboard"></i> Copy All JSON </button> <button class="btn btn-sm btn-outline-success me-1" onclick="copyPrettyResponse()"> <i class="bi bi-clipboard-plus"></i> Copy Pretty JSON </button> <button class="btn btn-sm btn-outline-secondary" onclick="toggleJsonExpansion()"> <i class="bi bi-arrows-expand" id="expandIcon"></i> <span id="expandText">Expand All</span> </button> </div> </div> <div class="response-metadata"> <span class="badge bg-success me-2">OAuth2 Token Response</span> <span class="badge bg-info me-2">Authorization Code Flow</span> <small class="text-muted"> <i class="bi bi-clock"></i> ${new Date().toLocaleString()} </small> </div> </div> <div class="response-body"> <div id="jsonViewer" class="json-response"></div> </div> </div> `; requestResponseArea.innerHTML = responseHTML; // Store response globally for copy functions window.currentApiResponse = response; // Initialize JSON viewer with copy buttons $('#jsonViewer').jsonViewer(response, { collapsed: false, rootCollapsible: false, withQuotes: true, withLinks: false }); // Add copy buttons to JSON values setTimeout(() => { addJsonCopyButtons(); }, 100); showToast('🎉 Authorization successful! Original token response displayed below.', 'success'); } // Check for original token response on page load function checkForOriginalTokenResponse() { // Check URL parameters for show_response flag const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('show_response') === 'true') { // The response should be available from the server-side template if (window.originalTokenResponse) { displayOriginalTokenResponse(window.originalTokenResponse); // Clean up URL parameters window.history.replaceState({}, document.title, window.location.pathname); } } }