diff --git a/api-docs/scripts/dist/generate-openapi-articles.js b/api-docs/scripts/dist/generate-openapi-articles.js index 46f2cd46d..7b32ce7f4 100644 --- a/api-docs/scripts/dist/generate-openapi-articles.js +++ b/api-docs/scripts/dist/generate-openapi-articles.js @@ -286,6 +286,7 @@ function generateTagPagesFromArticleData(options) { menuParent, productDescription, skipParentMenu, + pathSpecFiles, } = options; const yaml = require('js-yaml'); const articlesFile = path.join(articlesPath, 'articles.yml'); @@ -391,6 +392,7 @@ ${yaml.dump(frontmatter)}--- generateOperationPages({ articlesPath, contentPath, + pathSpecFiles, }); } /** @@ -410,12 +412,16 @@ function apiPathToSlug(apiPath) { * Generate standalone Hugo content pages for each API operation * * Creates individual pages at path-based URLs like /api/write/post/ - * for each operation, using RapiDoc Mini with tag-level specs. + * for each operation, using RapiDoc Mini. + * + * When pathSpecFiles is provided, uses path-specific specs for single-operation + * rendering (filters by method only, avoiding path prefix conflicts). + * Falls back to tag-based specs when pathSpecFiles is not available. * * @param options - Generation options */ function generateOperationPages(options) { - const { articlesPath, contentPath } = options; + const { articlesPath, contentPath, pathSpecFiles } = options; const yaml = require('js-yaml'); const articlesFile = path.join(articlesPath, 'articles.yml'); if (!fs.existsSync(articlesFile)) { @@ -453,15 +459,22 @@ function generateOperationPages(options) { } // Build frontmatter const title = op.summary || `${op.method} ${op.path}`; + // Determine spec file and match-paths based on availability of path-specific specs + // Path-specific specs isolate the path at file level, so we only filter by method + // This avoids substring matching issues (e.g., /admin matching /admin/regenerate) + const pathSpecFile = pathSpecFiles?.get(op.path); + const specFile = pathSpecFile || tagSpecFile; + const matchPaths = pathSpecFile ? method : `${method} ${op.path}`; const frontmatter = { title, description: `API reference for ${op.method} ${op.path}`, type: 'api-operation', layout: 'operation', // RapiDoc Mini configuration - specFile: tagSpecFile, - // RapiDoc match-paths format: "method /path" (e.g., "post /write") - matchPaths: `${method} ${op.path}`, + specFile, + // When using path-specific spec: just method (e.g., "post") + // When using tag spec: method + path (e.g., "post /write") + matchPaths, // Operation metadata operationId: op.operationId, method: op.method, @@ -630,6 +643,16 @@ function processProduct(productKey, config) { articleOutPath: articlesPath, includePaths: true, // Also generate path-based files for backwards compatibility }); + // Step 5b: Generate path-specific specs for operation pages + // Each path gets its own spec file, enabling method-only filtering + // This avoids substring matching issues (e.g., /admin matching /admin/regenerate) + console.log( + `\nšŸ“‹ Generating path-specific specs in ${staticPathsPath}...` + ); + const pathSpecFiles = openapiPathsToHugo.generatePathSpecificSpecs( + config.specFile, + staticPathsPath + ); // Step 6: Generate Hugo content pages from tag-based article data generateTagPagesFromArticleData({ articlesPath, @@ -637,6 +660,7 @@ function processProduct(productKey, config) { menuKey: config.menuKey, menuParent: 'InfluxDB HTTP API', skipParentMenu: config.skipParentMenu, + pathSpecFiles, }); } else { // Path-based generation: group paths by URL prefix (legacy) diff --git a/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js b/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js index 09e66f4f8..a46dfe199 100644 --- a/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js +++ b/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js @@ -64,8 +64,10 @@ var __importStar = }; })(); Object.defineProperty(exports, '__esModule', { value: true }); +exports.writePathSpecificSpecs = writePathSpecificSpecs; exports.generateHugoDataByTag = generateHugoDataByTag; exports.generateHugoData = generateHugoData; +exports.generatePathSpecificSpecs = generatePathSpecificSpecs; const yaml = __importStar(require('js-yaml')); const fs = __importStar(require('fs')); const path = __importStar(require('path')); @@ -303,6 +305,91 @@ function writeTagOpenapis(openapi, prefix, outPath) { } }); } +/** + * Convert API path to filename-safe slug + * + * @param apiPath - API path (e.g., "/api/v3/configure/token/admin") + * @returns Filename-safe slug (e.g., "api-v3-configure-token-admin") + */ +function pathToFileSlug(apiPath) { + return apiPath + .replace(/^\//, '') // Remove leading slash + .replace(/\//g, '-') // Replace slashes with dashes + .replace(/[{}]/g, '') // Remove curly braces from path params + .replace(/-+/g, '-') // Collapse multiple dashes + .replace(/-$/, ''); // Remove trailing dash +} +/** + * Write path-specific OpenAPI specs (one file per exact API path) + * + * Each file contains all HTTP methods for a single path, enabling + * operation pages to filter by method only (no path prefix conflicts). + * + * @param openapi - OpenAPI document + * @param outPath - Output directory path (e.g., "static/openapi/{product}/paths") + * @returns Map of API path to spec file path (for use in frontmatter) + */ +function writePathSpecificSpecs(openapi, outPath) { + const pathSpecFiles = new Map(); + if (!fs.existsSync(outPath)) { + fs.mkdirSync(outPath, { recursive: true }); + } + Object.entries(openapi.paths).forEach(([apiPath, pathItem]) => { + // Deep clone pathItem to avoid mutating original + const clonedPathItem = JSON.parse(JSON.stringify(pathItem)); + // Limit each operation to a single tag to prevent duplicate rendering in RapiDoc + // RapiDoc renders operations once per tag, so multiple tags cause duplicates + const usedTags = new Set(); + HTTP_METHODS.forEach((method) => { + const operation = clonedPathItem[method]; + if (operation?.tags && operation.tags.length > 0) { + // Select the most specific tag to avoid duplicate rendering + // Prefer "Auth token" over "Authentication" for token-related operations + let primaryTag = operation.tags[0]; + if (operation.tags.includes('Auth token')) { + primaryTag = 'Auth token'; + } + operation.tags = [primaryTag]; + usedTags.add(primaryTag); + } + }); + // Create spec with just this path (all its methods) + // Include global security requirements so RapiDoc displays auth correctly + const pathSpec = { + openapi: openapi.openapi, + info: { + ...openapi.info, + title: apiPath, + description: `API reference for ${apiPath}`, + }, + paths: { [apiPath]: clonedPathItem }, + components: openapi.components, // Include for $ref resolution + servers: openapi.servers, + security: openapi.security, // Global security requirements + }; + // Filter spec-level tags to only include those used by operations + if (openapi.tags) { + pathSpec.tags = openapi.tags.filter( + (tag) => usedTags.has(tag.name) && !tag['x-traitTag'] + ); + } + // Write files + const slug = pathToFileSlug(apiPath); + const yamlPath = path.resolve(outPath, `${slug}.yaml`); + const jsonPath = path.resolve(outPath, `${slug}.json`); + writeDataFile(pathSpec, yamlPath); + writeJsonFile(pathSpec, jsonPath); + // Store the web-accessible path (without "static/" prefix) + // Hugo serves files from static/ at the root, so we extract the path after 'static/' + const staticMatch = yamlPath.match(/static\/(.+)$/); + const webPath = staticMatch ? `/${staticMatch[1]}` : yamlPath; + pathSpecFiles.set(apiPath, webPath); + }); + console.log( + `Generated ${pathSpecFiles.size} path-specific specs in ${outPath}` + ); + return pathSpecFiles; +} /** * Write OpenAPI specs grouped by path to separate files * Generates both YAML and JSON versions @@ -769,9 +856,24 @@ function generateHugoData(options) { }); console.log('\nGeneration complete!\n'); } +/** + * Generate path-specific OpenAPI specs from a spec file + * + * Convenience wrapper that reads the spec file and generates path-specific specs. + * + * @param specFile - Path to OpenAPI spec file + * @param outPath - Output directory for path-specific specs + * @returns Map of API path to spec file web path (for use in frontmatter) + */ +function generatePathSpecificSpecs(specFile, outPath) { + const openapi = readFile(specFile, 'utf8'); + return writePathSpecificSpecs(openapi, outPath); +} // CommonJS export for backward compatibility module.exports = { generateHugoData, generateHugoDataByTag, + generatePathSpecificSpecs, + writePathSpecificSpecs, }; //# sourceMappingURL=index.js.map diff --git a/api-docs/scripts/generate-openapi-articles.ts b/api-docs/scripts/generate-openapi-articles.ts index 12db38fea..1b3abba40 100644 --- a/api-docs/scripts/generate-openapi-articles.ts +++ b/api-docs/scripts/generate-openapi-articles.ts @@ -320,6 +320,8 @@ interface GenerateTagPagesOptions { productDescription?: string; /** Skip adding menu entry to generated parent page */ skipParentMenu?: boolean; + /** Map of API path to path-specific spec file (for single-operation rendering) */ + pathSpecFiles?: Map; } /** @@ -341,6 +343,7 @@ function generateTagPagesFromArticleData( menuParent, productDescription, skipParentMenu, + pathSpecFiles, } = options; const yaml = require('js-yaml'); const articlesFile = path.join(articlesPath, 'articles.yml'); @@ -484,6 +487,7 @@ ${yaml.dump(frontmatter)}--- generateOperationPages({ articlesPath, contentPath, + pathSpecFiles, }); } @@ -495,6 +499,8 @@ interface GenerateOperationPagesOptions { articlesPath: string; /** Output path for generated content pages */ contentPath: string; + /** Map of API path to path-specific spec file (for single-operation rendering) */ + pathSpecFiles?: Map; } /** @@ -515,12 +521,16 @@ function apiPathToSlug(apiPath: string): string { * Generate standalone Hugo content pages for each API operation * * Creates individual pages at path-based URLs like /api/write/post/ - * for each operation, using RapiDoc Mini with tag-level specs. + * for each operation, using RapiDoc Mini. + * + * When pathSpecFiles is provided, uses path-specific specs for single-operation + * rendering (filters by method only, avoiding path prefix conflicts). + * Falls back to tag-based specs when pathSpecFiles is not available. * * @param options - Generation options */ function generateOperationPages(options: GenerateOperationPagesOptions): void { - const { articlesPath, contentPath } = options; + const { articlesPath, contentPath, pathSpecFiles } = options; const yaml = require('js-yaml'); const articlesFile = path.join(articlesPath, 'articles.yml'); @@ -580,15 +590,24 @@ function generateOperationPages(options: GenerateOperationPagesOptions): void { // Build frontmatter const title = op.summary || `${op.method} ${op.path}`; + + // Determine spec file and match-paths based on availability of path-specific specs + // Path-specific specs isolate the path at file level, so we only filter by method + // This avoids substring matching issues (e.g., /admin matching /admin/regenerate) + const pathSpecFile = pathSpecFiles?.get(op.path); + const specFile = pathSpecFile || tagSpecFile; + const matchPaths = pathSpecFile ? method : `${method} ${op.path}`; + const frontmatter: Record = { title, description: `API reference for ${op.method} ${op.path}`, type: 'api-operation', layout: 'operation', // RapiDoc Mini configuration - specFile: tagSpecFile, - // RapiDoc match-paths format: "method /path" (e.g., "post /write") - matchPaths: `${method} ${op.path}`, + specFile, + // When using path-specific spec: just method (e.g., "post") + // When using tag spec: method + path (e.g., "post /write") + matchPaths, // Operation metadata operationId: op.operationId, method: op.method, @@ -771,6 +790,17 @@ function processProduct(productKey: string, config: ProductConfig): void { includePaths: true, // Also generate path-based files for backwards compatibility }); + // Step 5b: Generate path-specific specs for operation pages + // Each path gets its own spec file, enabling method-only filtering + // This avoids substring matching issues (e.g., /admin matching /admin/regenerate) + console.log( + `\nšŸ“‹ Generating path-specific specs in ${staticPathsPath}...` + ); + const pathSpecFiles = openapiPathsToHugo.generatePathSpecificSpecs( + config.specFile, + staticPathsPath + ); + // Step 6: Generate Hugo content pages from tag-based article data generateTagPagesFromArticleData({ articlesPath, @@ -778,6 +808,7 @@ function processProduct(productKey: string, config: ProductConfig): void { menuKey: config.menuKey, menuParent: 'InfluxDB HTTP API', skipParentMenu: config.skipParentMenu, + pathSpecFiles, }); } else { // Path-based generation: group paths by URL prefix (legacy) diff --git a/api-docs/scripts/openapi-paths-to-hugo-data/index.ts b/api-docs/scripts/openapi-paths-to-hugo-data/index.ts index 8492230b6..172f60607 100644 --- a/api-docs/scripts/openapi-paths-to-hugo-data/index.ts +++ b/api-docs/scripts/openapi-paths-to-hugo-data/index.ts @@ -579,6 +579,106 @@ function writeTagOpenapis( }); } +/** + * Convert API path to filename-safe slug + * + * @param apiPath - API path (e.g., "/api/v3/configure/token/admin") + * @returns Filename-safe slug (e.g., "api-v3-configure-token-admin") + */ +function pathToFileSlug(apiPath: string): string { + return apiPath + .replace(/^\//, '') // Remove leading slash + .replace(/\//g, '-') // Replace slashes with dashes + .replace(/[{}]/g, '') // Remove curly braces from path params + .replace(/-+/g, '-') // Collapse multiple dashes + .replace(/-$/, ''); // Remove trailing dash +} + +/** + * Write path-specific OpenAPI specs (one file per exact API path) + * + * Each file contains all HTTP methods for a single path, enabling + * operation pages to filter by method only (no path prefix conflicts). + * + * @param openapi - OpenAPI document + * @param outPath - Output directory path (e.g., "static/openapi/{product}/paths") + * @returns Map of API path to spec file path (for use in frontmatter) + */ +export function writePathSpecificSpecs( + openapi: OpenAPIDocument, + outPath: string +): Map { + const pathSpecFiles = new Map(); + + if (!fs.existsSync(outPath)) { + fs.mkdirSync(outPath, { recursive: true }); + } + + Object.entries(openapi.paths).forEach(([apiPath, pathItem]) => { + // Deep clone pathItem to avoid mutating original + const clonedPathItem: PathItem = JSON.parse(JSON.stringify(pathItem)); + + // Limit each operation to a single tag to prevent duplicate rendering in RapiDoc + // RapiDoc renders operations once per tag, so multiple tags cause duplicates + const usedTags = new Set(); + HTTP_METHODS.forEach((method) => { + const operation = clonedPathItem[method] as Operation | undefined; + if (operation?.tags && operation.tags.length > 0) { + // Select the most specific tag to avoid duplicate rendering + // Prefer "Auth token" over "Authentication" for token-related operations + let primaryTag = operation.tags[0]; + if (operation.tags.includes('Auth token')) { + primaryTag = 'Auth token'; + } + operation.tags = [primaryTag]; + usedTags.add(primaryTag); + } + }); + + // Create spec with just this path (all its methods) + // Include global security requirements so RapiDoc displays auth correctly + const pathSpec: OpenAPIDocument = { + openapi: openapi.openapi, + info: { + ...openapi.info, + title: apiPath, + description: `API reference for ${apiPath}`, + }, + paths: { [apiPath]: clonedPathItem }, + components: openapi.components, // Include for $ref resolution + servers: openapi.servers, + security: openapi.security, // Global security requirements + }; + + // Filter spec-level tags to only include those used by operations + if (openapi.tags) { + pathSpec.tags = openapi.tags.filter( + (tag) => usedTags.has(tag.name) && !tag['x-traitTag'] + ); + } + + // Write files + const slug = pathToFileSlug(apiPath); + const yamlPath = path.resolve(outPath, `${slug}.yaml`); + const jsonPath = path.resolve(outPath, `${slug}.json`); + + writeDataFile(pathSpec, yamlPath); + writeJsonFile(pathSpec, jsonPath); + + // Store the web-accessible path (without "static/" prefix) + // Hugo serves files from static/ at the root, so we extract the path after 'static/' + const staticMatch = yamlPath.match(/static\/(.+)$/); + const webPath = staticMatch ? `/${staticMatch[1]}` : yamlPath; + pathSpecFiles.set(apiPath, webPath); + }); + + console.log( + `Generated ${pathSpecFiles.size} path-specific specs in ${outPath}` + ); + + return pathSpecFiles; +} + /** * Write OpenAPI specs grouped by path to separate files * Generates both YAML and JSON versions @@ -1138,8 +1238,27 @@ export function generateHugoData(options: GenerateHugoDataOptions): void { console.log('\nGeneration complete!\n'); } +/** + * Generate path-specific OpenAPI specs from a spec file + * + * Convenience wrapper that reads the spec file and generates path-specific specs. + * + * @param specFile - Path to OpenAPI spec file + * @param outPath - Output directory for path-specific specs + * @returns Map of API path to spec file web path (for use in frontmatter) + */ +export function generatePathSpecificSpecs( + specFile: string, + outPath: string +): Map { + const openapi = readFile(specFile, 'utf8'); + return writePathSpecificSpecs(openapi, outPath); +} + // CommonJS export for backward compatibility module.exports = { generateHugoData, generateHugoDataByTag, + generatePathSpecificSpecs, + writePathSpecificSpecs, }; diff --git a/assets/js/components/api-auth-input.ts b/assets/js/components/api-auth-input.ts index aa5f0b0e2..dc37965e4 100644 --- a/assets/js/components/api-auth-input.ts +++ b/assets/js/components/api-auth-input.ts @@ -1,8 +1,22 @@ /** - * API Auth Input Component + * API Auth Input Component (Popover) * - * Provides credential input fields for API operations. - * Stores credentials in sessionStorage for "Try it" requests. + * Provides a popover-based credential input for API operations. + * Integrates with RapiDoc's auth system via JavaScript API. + * + * Features: + * - Popover UI triggered by button click + * - Filters auth schemes based on operation requirements + * - Session-only credentials (not persisted to storage) + * - Syncs with RapiDoc's "Try it" feature + * + * Usage: + * + * */ interface ComponentOptions { @@ -12,102 +26,428 @@ interface ComponentOptions { interface AuthCredentials { bearer?: string; basic?: { username: string; password: string }; - apiKey?: string; + querystring?: string; } -const STORAGE_KEY = 'influxdb_api_credentials'; +type CleanupFn = () => void; + +// In-memory credential storage (not persisted) +let currentCredentials: AuthCredentials = {}; /** - * Get stored credentials from sessionStorage + * Get current credentials (in-memory only) */ function getCredentials(): AuthCredentials { - try { - const stored = sessionStorage.getItem(STORAGE_KEY); - return stored ? JSON.parse(stored) : {}; - } catch { - return {}; - } + return currentCredentials; } /** - * Store credentials in sessionStorage + * Set credentials (in-memory only, not persisted) */ function setCredentials(credentials: AuthCredentials): void { - sessionStorage.setItem(STORAGE_KEY, JSON.stringify(credentials)); + currentCredentials = credentials; } /** - * Create the auth input form + * Check if any credentials are set */ -function createAuthForm(schemes: string[]): HTMLElement { - const form = document.createElement('div'); - form.className = 'api-auth-form'; - form.innerHTML = ` -

Authentication

-

Enter credentials to use with "Try it" requests.

- ${ - schemes.includes('bearer') - ? ` -
- - -
- ` - : '' +function hasCredentials(): boolean { + return !!( + currentCredentials.bearer || + currentCredentials.basic?.password || + currentCredentials.querystring + ); +} + +/** + * Try to update the visible auth input in RapiDoc's shadow DOM. + * This provides visual feedback but is not essential for authentication. + */ +function updateRapiDocAuthInput( + rapiDoc: HTMLElement, + token: string, + scheme: 'bearer' | 'token' +): void { + try { + const shadowRoot = rapiDoc.shadowRoot; + if (!shadowRoot) return; + + const headerValue = + scheme === 'bearer' ? `Bearer ${token}` : `Token ${token}`; + + const authInputSelectors = [ + 'input[data-pname="Authorization"]', + 'input[placeholder*="authorization" i]', + 'input[placeholder*="token" i]', + '.request-headers input[type="text"]', + ]; + + for (const selector of authInputSelectors) { + const input = shadowRoot.querySelector(selector); + if (input && !input.value) { + input.value = headerValue; + input.dispatchEvent(new Event('input', { bubbles: true })); + console.log('[API Auth] Updated visible auth input in RapiDoc'); + return; + } } - ${ - schemes.includes('basic') - ? ` -
- - -
-
- - -
- ` - : '' + } catch (e) { + console.debug('[API Auth] Could not update visible input:', e); + } +} + +/** + * Apply credentials to a RapiDoc element + * Returns true if credentials were successfully applied + */ +function applyCredentialsToRapiDoc( + rapiDoc: HTMLElement, + credentials: AuthCredentials +): boolean { + let applied = false; + + // Clear existing credentials first + if ('removeAllSecurityKeys' in rapiDoc) { + try { + (rapiDoc as any).removeAllSecurityKeys(); + } catch (e) { + console.warn('[API Auth] Failed to clear existing credentials:', e); } - + } + + // Apply bearer/token credentials + if (credentials.bearer) { + try { + // Method 1: HTML attributes (most reliable) + rapiDoc.setAttribute('api-key-name', 'Authorization'); + rapiDoc.setAttribute('api-key-location', 'header'); + rapiDoc.setAttribute('api-key-value', `Bearer ${credentials.bearer}`); + console.log('[API Auth] Set auth via HTML attributes'); + + // Method 2: JavaScript API for scheme-specific auth + if ('setApiKey' in rapiDoc) { + (rapiDoc as any).setApiKey('BearerAuthentication', credentials.bearer); + (rapiDoc as any).setApiKey('TokenAuthentication', credentials.bearer); + console.log('[API Auth] Applied bearer/token via setApiKey()'); + } + + applied = true; + updateRapiDocAuthInput(rapiDoc, credentials.bearer, 'bearer'); + } catch (e) { + console.error('[API Auth] Failed to set API key:', e); + } + } + + // Apply basic auth credentials + if ('setHttpUserNameAndPassword' in rapiDoc && credentials.basic?.password) { + try { + (rapiDoc as any).setHttpUserNameAndPassword( + 'BasicAuthentication', + credentials.basic.username || '', + credentials.basic.password + ); + applied = true; + console.log('[API Auth] Applied basic auth credentials to RapiDoc'); + } catch (e) { + console.error('[API Auth] Failed to set basic auth:', e); + } + } + + return applied; +} + +/** + * Create auth field HTML for a specific scheme + */ +function createAuthField(scheme: string): string { + switch (scheme) { + case 'bearer': + return ` +
+ +
+ + +
+
`; + + case 'token': + return ` +
+ +
+ + +
+
`; + + case 'basic': + return ` +
+

Basic Authentication (v1 compatibility)

+
+ + +
+
+ +
+ + +
+
+
`; + + case 'querystring': + return ` +
+ +
+ + +
+
`; + + default: + return ''; + } +} + +/** + * Create the popover content HTML + */ +function createPopoverContent(schemes: string[]): string { + // If both bearer and token are supported, show combined field + const hasBearerAndToken = + schemes.includes('bearer') && schemes.includes('token'); + const displaySchemes = hasBearerAndToken + ? schemes.filter((s) => s !== 'token') + : schemes; + + const fields = displaySchemes.map((s) => createAuthField(s)).join(''); + + // Adjust label if both bearer and token are supported + const bearerLabel = hasBearerAndToken + ? '(Bearer / Token auth)' + : '(Bearer auth)'; + + return ` +
+
+

API Credentials

+ +
+

+ Enter credentials for "Try it" requests. +

+ ${fields.replace('(Bearer auth)', bearerLabel)} +
+ + +
+ +
`; - return form; } /** - * Initialize the auth input component + * Show feedback message */ -export default function ApiAuthInput({ component }: ComponentOptions): void { - const schemesAttr = component.dataset.schemes || 'bearer'; +function showFeedback( + container: HTMLElement, + message: string, + type: 'success' | 'error' +): void { + const feedback = container.querySelector('.auth-feedback'); + if (feedback) { + feedback.textContent = message; + feedback.className = `auth-feedback auth-feedback--${type}`; + feedback.hidden = false; + + setTimeout(() => { + feedback.hidden = true; + }, 3000); + } +} + +/** + * Update the status indicator on the trigger button + */ +function updateStatusIndicator(trigger: HTMLElement): void { + const indicator = trigger.querySelector( + '.auth-status-indicator' + ); + const hasCreds = hasCredentials(); + + if (indicator) { + indicator.hidden = !hasCreds; + } + + trigger.classList.toggle('has-credentials', hasCreds); +} + +/** + * Initialize the auth input popover component + */ +export default function ApiAuthInput({ + component, +}: ComponentOptions): CleanupFn | void { + // Component is the trigger button + const trigger = component; + const popoverEl = trigger.nextElementSibling as HTMLElement | null; + + if (!popoverEl || !popoverEl.classList.contains('api-auth-popover')) { + console.error('[API Auth] Popover container not found'); + return; + } + + // Now TypeScript knows popover is not null + const popover = popoverEl; + + const schemesAttr = trigger.dataset.schemes || 'bearer'; const schemes = schemesAttr.split(',').map((s) => s.trim().toLowerCase()); - const form = createAuthForm(schemes); - component.appendChild(form); + // Render popover content + popover.innerHTML = createPopoverContent(schemes); - // Load existing credentials - const credentials = getCredentials(); - const bearerInput = form.querySelector('#auth-bearer'); - const usernameInput = form.querySelector('#auth-username'); - const passwordInput = form.querySelector('#auth-password'); + // Element references + const bearerInput = popover.querySelector('#auth-bearer'); + const tokenInput = popover.querySelector('#auth-token'); + const usernameInput = + popover.querySelector('#auth-username'); + const passwordInput = + popover.querySelector('#auth-password'); + const querystringInput = + popover.querySelector('#auth-querystring'); + const applyBtn = popover.querySelector('.auth-apply'); + const clearBtn = popover.querySelector('.auth-clear'); + const closeBtn = popover.querySelector('.popover-close'); - if (bearerInput && credentials.bearer) { - bearerInput.value = credentials.bearer; - } - if (usernameInput && credentials.basic?.username) { - usernameInput.value = credentials.basic.username; - } - if (passwordInput && credentials.basic?.password) { - passwordInput.value = credentials.basic.password; + /** + * Toggle popover visibility + */ + function togglePopover(show?: boolean): void { + const shouldShow = show ?? popover.hidden; + popover.hidden = !shouldShow; + trigger.setAttribute('aria-expanded', String(shouldShow)); + + if (shouldShow) { + // Focus first input when opening + const firstInput = popover.querySelector( + 'input:not([type="hidden"])' + ); + firstInput?.focus(); + } } - // Save button handler - const saveBtn = form.querySelector('.auth-save'); - saveBtn?.addEventListener('click', () => { + /** + * Close popover + */ + function closePopover(): void { + togglePopover(false); + trigger.focus(); + } + + // Trigger button click + trigger.addEventListener('click', (e) => { + e.stopPropagation(); + togglePopover(); + }); + + // Close button + closeBtn?.addEventListener('click', closePopover); + + // Close on outside click + function handleOutsideClick(e: MouseEvent): void { + if ( + !popover.hidden && + !popover.contains(e.target as Node) && + !trigger.contains(e.target as Node) + ) { + closePopover(); + } + } + document.addEventListener('click', handleOutsideClick); + + // Close on Escape + function handleEscape(e: KeyboardEvent): void { + if (e.key === 'Escape' && !popover.hidden) { + closePopover(); + } + } + document.addEventListener('keydown', handleEscape); + + // Show/hide toggle for password fields + const showToggles = + popover.querySelectorAll('.auth-show-toggle'); + showToggles.forEach((btn) => { + btn.addEventListener('click', () => { + const targetId = btn.dataset.target; + const input = popover.querySelector(`#${targetId}`); + if (input) { + const isPassword = input.type === 'password'; + input.type = isPassword ? 'text' : 'password'; + btn.classList.toggle('showing', !isPassword); + } + }); + }); + + /** + * Apply credentials + */ + function applyCredentials(): void { const newCredentials: AuthCredentials = {}; - if (bearerInput?.value) { - newCredentials.bearer = bearerInput.value; + // Get token from bearer or token input (they're combined for UX) + const tokenValue = bearerInput?.value || tokenInput?.value; + if (tokenValue) { + newCredentials.bearer = tokenValue; } + if (usernameInput?.value || passwordInput?.value) { newCredentials.basic = { username: usernameInput?.value || '', @@ -115,14 +455,105 @@ export default function ApiAuthInput({ component }: ComponentOptions): void { }; } - setCredentials(newCredentials); - - // Notify RapiDoc of new credentials - const rapiDoc = document.querySelector('rapi-doc'); - if (rapiDoc && 'setApiKey' in rapiDoc) { - (rapiDoc as any).setApiKey(newCredentials.bearer || ''); + if (querystringInput?.value) { + newCredentials.querystring = querystringInput.value; } - alert('Credentials saved for this session'); + setCredentials(newCredentials); + updateStatusIndicator(trigger); + + // Apply to RapiDoc + const rapiDoc = document.querySelector('rapi-doc') as HTMLElement | null; + if (rapiDoc && 'setApiKey' in rapiDoc) { + const applied = applyCredentialsToRapiDoc(rapiDoc, newCredentials); + if (applied) { + showFeedback(popover, 'Credentials applied', 'success'); + } else { + showFeedback(popover, 'No credentials to apply', 'error'); + } + } else { + showFeedback(popover, 'Saved (API viewer loading...)', 'success'); + } + } + + /** + * Clear credentials + */ + function clearCredentials(): void { + if (bearerInput) bearerInput.value = ''; + if (tokenInput) tokenInput.value = ''; + if (usernameInput) usernameInput.value = ''; + if (passwordInput) passwordInput.value = ''; + if (querystringInput) querystringInput.value = ''; + + setCredentials({}); + updateStatusIndicator(trigger); + + // Clear from RapiDoc + const rapiDoc = document.querySelector('rapi-doc') as HTMLElement | null; + if (rapiDoc) { + rapiDoc.removeAttribute('api-key-name'); + rapiDoc.removeAttribute('api-key-location'); + rapiDoc.removeAttribute('api-key-value'); + + if ('removeAllSecurityKeys' in rapiDoc) { + try { + (rapiDoc as any).removeAllSecurityKeys(); + } catch (e) { + console.debug('[API Auth] Failed to clear RapiDoc credentials:', e); + } + } + } + + showFeedback(popover, 'Credentials cleared', 'success'); + } + + // Button handlers + applyBtn?.addEventListener('click', applyCredentials); + clearBtn?.addEventListener('click', clearCredentials); + + // Listen for RapiDoc spec-loaded event to apply stored credentials + function handleSpecLoaded(event: Event): void { + const rapiDoc = event.target as HTMLElement; + const storedCredentials = getCredentials(); + if ( + storedCredentials.bearer || + storedCredentials.basic?.password || + storedCredentials.querystring + ) { + setTimeout(() => { + applyCredentialsToRapiDoc(rapiDoc, storedCredentials); + }, 100); + } + } + + // Watch for RapiDoc elements + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node instanceof HTMLElement && node.tagName === 'RAPI-DOC') { + node.addEventListener('spec-loaded', handleSpecLoaded); + } + } + } }); + + observer.observe(document.body, { childList: true, subtree: true }); + + // Check if RapiDoc already exists + const existingRapiDoc = document.querySelector('rapi-doc'); + if (existingRapiDoc) { + existingRapiDoc.addEventListener('spec-loaded', handleSpecLoaded); + } + + // Initialize status indicator + updateStatusIndicator(trigger); + + // Cleanup function + return (): void => { + observer.disconnect(); + document.removeEventListener('click', handleOutsideClick); + document.removeEventListener('keydown', handleEscape); + existingRapiDoc?.removeEventListener('spec-loaded', handleSpecLoaded); + }; } diff --git a/assets/js/components/rapidoc-mini.ts b/assets/js/components/rapidoc-mini.ts index cf5f7c724..5e3f63759 100644 --- a/assets/js/components/rapidoc-mini.ts +++ b/assets/js/components/rapidoc-mini.ts @@ -156,20 +156,56 @@ function applyTheme(element: HTMLElement): void { element.setAttribute('nav-accent-color', config.primaryColor); } +/** + * Build match pattern that identifies operations within a spec. + * + * When using path-specific specs (recommended), the spec contains only one path, + * so matchPaths is just the HTTP method (e.g., "post"). The path isolation at the + * file level prevents substring matching issues - no title needed. + * + * When using tag-based specs (fallback), matchPaths includes the full path + * (e.g., "post /api/v3/configure/token/admin"). Adding the title helps differentiate + * operations whose paths are prefixes of each other. + * + * RapiDoc's search string format: + * `${method} ${path} ${summary} ${description} ${operationId} ${tagName}`.toLowerCase() + * + * @param matchPaths - The match pattern: just method for path-specific specs, + * or "method /path" for tag-based specs + * @param title - Optional page title to append (only used for tag-based specs) + * @returns Pattern for RapiDoc's match-paths attribute + */ +function buildMatchPattern(matchPaths: string, title?: string): string { + // Detect path-specific spec mode: matchPaths is just an HTTP method (no path) + const isMethodOnly = /^(get|post|put|patch|delete|options|head|trace)$/i.test( + matchPaths.trim() + ); + + // For path-specific specs: use method only, title not needed (path isolated at file level) + // For tag-based specs: append title to differentiate prefix conflicts + if (title && !isMethodOnly) { + return `${matchPaths} ${title.toLowerCase()}`; + } + return matchPaths; +} + /** * Create RapiDoc Mini element with configuration */ function createRapiDocElement( specUrl: string, - matchPaths?: string + matchPaths?: string, + title?: string ): HTMLElement { const element = document.createElement(RAPIDOC_ELEMENT); // Core attributes element.setAttribute('spec-url', specUrl); - // matchPaths format: "method /path" (e.g., "post /write") + + // Set match-paths filter. With path-specific specs, this is just the method. + // With tag-based specs, includes path + optional title for uniqueness. if (matchPaths) { - element.setAttribute('match-paths', matchPaths); + element.setAttribute('match-paths', buildMatchPattern(matchPaths, title)); } // Typography - match docs theme fonts @@ -234,8 +270,11 @@ function createRapiDocElement( element.setAttribute('use-path-in-nav-bar', 'false'); element.setAttribute('show-info', 'false'); - // Authentication display - allow-authentication enables proper layout - element.setAttribute('allow-authentication', 'true'); + // Authentication display - hide RapiDoc's built-in auth section + // We use a custom popover component for credential input instead + // Credentials are applied via HTML attributes (api-key-name, api-key-value) + // and the setApiKey() JavaScript API + element.setAttribute('allow-authentication', 'false'); element.setAttribute('show-components', 'false'); // Custom CSS for internal style overrides (table layout, etc.) @@ -335,6 +374,7 @@ export default async function RapiDocMini({ // Get configuration from data attributes const specUrl = component.dataset.specUrl; const matchPaths = component.dataset.matchPaths; + const title = component.dataset.title; if (!specUrl) { console.error('[RapiDoc Mini] No data-spec-url attribute provided'); @@ -355,7 +395,7 @@ export default async function RapiDocMini({ } // Create and append RapiDoc Mini element - const rapiDocElement = createRapiDocElement(specUrl, matchPaths); + const rapiDocElement = createRapiDocElement(specUrl, matchPaths, title); component.appendChild(rapiDocElement); // Watch for theme changes and return cleanup function diff --git a/assets/styles/layouts/_api-security-schemes.scss b/assets/styles/layouts/_api-security-schemes.scss index ee0860d5c..325ec2fa2 100644 --- a/assets/styles/layouts/_api-security-schemes.scss +++ b/assets/styles/layouts/_api-security-schemes.scss @@ -72,51 +72,272 @@ } //////////////////////////////////////////////////////////////////////////////// -// API Auth Form - Custom credential input for operation pages +// API Auth Popover - Credential input for operation pages +// +// Popover-based UI triggered by "Set credentials" button. +// Positioned above RapiDoc, integrates with "Try it" via JavaScript API. //////////////////////////////////////////////////////////////////////////////// -.api-auth-form { - margin-bottom: 1.5rem; - padding: 1rem; - background: $g3-castle; +.api-auth-trigger-wrapper { + position: relative; + display: inline-block; + margin-bottom: 1rem; +} + +.api-auth-trigger { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.75rem; + font-size: 0.85rem; + font-weight: 500; + color: $article-text; + background: $g20-white; border: 1px solid $g5-pepper; border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; - h4 { - margin: 0 0 0.5rem 0; + &:hover { + background: rgba($b-pool, 0.08); + border-color: $b-pool; } - .auth-form-description { - margin: 0 0 1rem 0; - font-size: 0.9rem; + &:focus { + outline: 2px solid $b-pool; + outline-offset: 2px; + } + + &.has-credentials { + border-color: $gr-viridian; + background: rgba($gr-viridian, 0.08); + } + + .auth-icon { color: $g9-mountain; } - .auth-field { - margin-bottom: 1rem; - - label { - display: block; - margin-bottom: 0.25rem; - font-weight: 600; - font-size: 0.9rem; - } - - input { - width: 100%; - padding: 0.5rem; - border: 1px solid $g5-pepper; - border-radius: 3px; - font-family: inherit; - } - } - - .auth-save { - margin-top: 0.5rem; + &.has-credentials .auth-icon { + color: $gr-viridian; } } -// Dark theme overrides - using CSS selectors (no mixin in this codebase) +.auth-status-indicator { + width: 8px; + height: 8px; + background: $gr-viridian; + border-radius: 50%; + margin-left: 0.25rem; +} + +.api-auth-popover { + position: absolute; + top: calc(100% + 8px); + left: 0; + z-index: 1000; + min-width: 320px; + max-width: 400px; + background: $g20-white; + border: 1px solid $g5-pepper; + border-radius: 6px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + + &[hidden] { + display: none; + } +} + +.api-auth-popover-content { + padding: 1rem; +} + +.popover-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; + + h4 { + margin: 0; + font-size: 0.95rem; + font-weight: 600; + color: $article-heading; + } +} + +.popover-close { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + background: none; + border: none; + border-radius: 4px; + cursor: pointer; + color: $g9-mountain; + transition: all 0.15s; + + &:hover { + background: rgba(0, 0, 0, 0.05); + color: $article-text; + } +} + +.auth-description { + margin: 0 0 1rem 0; + font-size: 0.85rem; + color: $g9-mountain; +} + +.auth-field { + margin-bottom: 0.75rem; + + label { + display: block; + margin-bottom: 0.25rem; + font-weight: 600; + font-size: 0.85rem; + color: $article-text; + } + + .auth-label-text { + margin-right: 0.25rem; + } + + .auth-label-hint { + font-weight: 400; + color: $g9-mountain; + } + + input { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid $g5-pepper; + border-radius: 3px; + font-family: inherit; + font-size: 0.9rem; + background: $g20-white; + color: $article-text; + + &:focus { + outline: none; + border-color: $b-pool; + box-shadow: 0 0 0 2px rgba($b-pool, 0.2); + } + + &::placeholder { + color: $g9-mountain; + } + } +} + +.auth-input-group { + position: relative; + display: flex; + align-items: center; + + input { + padding-right: 2.5rem; + } +} + +.auth-show-toggle { + position: absolute; + right: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem; + background: none; + border: none; + cursor: pointer; + color: $g9-mountain; + opacity: 0.6; + transition: opacity 0.15s; + + &:hover { + opacity: 1; + } + + &.showing { + color: $b-pool; + opacity: 1; + } +} + +.auth-field-group { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid $g5-pepper; + + .auth-group-label { + margin: 0 0 0.75rem 0; + font-weight: 600; + font-size: 0.85rem; + color: $article-text; + } +} + +.auth-actions { + display: flex; + gap: 0.5rem; + margin-top: 1rem; + + // Explicit button styling to avoid link-like appearance + .auth-apply, + .auth-clear { + padding: 0.4rem 0.75rem; + font-size: 0.85rem; + font-weight: 500; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; + text-decoration: none; + } + + .auth-apply { + background: $b-pool; + color: $g20-white; + border: 1px solid $b-pool; + + &:hover { + background: darken($b-pool, 8%); + border-color: darken($b-pool, 8%); + } + } + + .auth-clear { + background: transparent; + color: $article-text; + border: 1px solid $g5-pepper; + + &:hover { + background: rgba(0, 0, 0, 0.05); + border-color: $g9-mountain; + } + } +} + +.auth-feedback { + margin: 0.75rem 0 0 0; + padding: 0.5rem 0.75rem; + font-size: 0.85rem; + border-radius: 3px; + + &.auth-feedback--success { + background: rgba($gr-viridian, 0.1); + color: $gr-viridian; + } + + &.auth-feedback--error { + background: rgba($r-fire, 0.1); + color: $r-fire; + } +} + +// Dark theme overrides [data-theme="dark"], html:has(link[title="dark-theme"]:not([disabled])) { .api-security-schemes { @@ -142,18 +363,127 @@ html:has(link[title="dark-theme"]:not([disabled])) { } } - .api-auth-form { + .api-auth-trigger { background: $grey15; border-color: $grey25; + color: $g20-white; - .auth-form-description { + &:hover { + background: rgba($b-pool, 0.1); + border-color: $b-pool; + } + + &.has-credentials { + border-color: $gr-viridian; + background: rgba($gr-viridian, 0.1); + } + + .auth-icon { color: $g15-platinum; } - .auth-field input { + &.has-credentials .auth-icon { + color: $gr-emerald; + } + } + + .api-auth-popover { + background: $grey15; + border-color: $grey25; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + } + + .popover-header h4 { + color: $g20-white; + } + + .popover-close { + color: $g15-platinum; + + &:hover { background: $grey20; - border-color: $grey25; color: $g20-white; } } + + .auth-description { + color: $g15-platinum; + } + + .auth-field { + label { + color: $g20-white; + } + + .auth-label-hint { + color: $g15-platinum; + } + + input { + background: $grey20; + border-color: $grey25; + color: $g20-white; + + &:focus { + border-color: $b-pool; + } + + &::placeholder { + color: $g9-mountain; + } + } + } + + .auth-show-toggle { + color: $g15-platinum; + + &.showing { + color: $b-pool; + } + } + + .auth-field-group { + border-top-color: $grey25; + + .auth-group-label { + color: $g20-white; + } + } + + .auth-feedback { + &.auth-feedback--success { + background: rgba($gr-viridian, 0.15); + color: $gr-emerald; + } + + &.auth-feedback--error { + background: rgba($r-fire, 0.15); + color: $r-tungsten; + } + } + + .auth-actions { + .auth-apply { + background: $b-pool; + color: $g20-white; + border-color: $b-pool; + + &:hover { + background: lighten($b-pool, 5%); + border-color: lighten($b-pool, 5%); + } + } + + .auth-clear { + background: transparent; + color: $g15-platinum; + border-color: $grey25; + + &:hover { + background: $grey20; + border-color: $g15-platinum; + color: $g20-white; + } + } + } } diff --git a/data/article_data/influxdb/influxdb3_core/articles.json b/data/article_data/influxdb/influxdb3_core/articles.json index ff6c0f9c0..276c9b278 100644 --- a/data/article_data/influxdb/influxdb3_core/articles.json +++ b/data/article_data/influxdb/influxdb3_core/articles.json @@ -63,26 +63,16 @@ "fields": { "name": "Authentication", "describes": [ - "/api/v3/configure/token/admin", "/api/v3/configure/token/admin/regenerate", "/api/v3/configure/token", "/api/v3/configure/token/named_admin" ], "title": "Authentication", - "description": "Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API:\n\n| Authentication scheme | Works with |\n|:----------------------|:-----------|\n| Bearer authentication | All endpoints |\n| Token authentication | v1, v2 endpoints |\n| Basic authentication | v1 endpoints |\n| Querystring authentication | v1 endpoints |\n\nSee the **Security Schemes** section below for details on each authentication method.\n", + "description": "Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API:\n\n| Authentication scheme | Works with |\n|:----------------------|:-----------|\n| Bearer authentication | All endpoints |\n| Token authentication | v1 and v2 compatibility endpoints (`/write`, `/query`, `/api/v2/write`) |\n| Basic authentication | v1 compatibility endpoints (`/write`, `/query`) |\n| Querystring authentication | v1 compatibility endpoints (`/write`, `/query`) |\n\nSee the **Security Schemes** section below for details on each authentication method.\n", "tag": "Authentication", "isConceptual": true, "menuGroup": "Concepts", "operations": [ - { - "operationId": "PostCreateAdminToken", - "method": "POST", - "path": "/api/v3/configure/token/admin", - "summary": "Create admin token", - "tags": [ - "Authentication" - ] - }, { "operationId": "PostRegenerateAdminToken", "method": "POST", @@ -111,7 +101,7 @@ ] } ], - "tagDescription": "Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API:\n\n| Authentication scheme | Works with |\n|:----------------------|:-----------|\n| Bearer authentication | All endpoints |\n| Token authentication | v1, v2 endpoints |\n| Basic authentication | v1 endpoints |\n| Querystring authentication | v1 endpoints |\n\nSee the **Security Schemes** section below for details on each authentication method.\n", + "tagDescription": "Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API:\n\n| Authentication scheme | Works with |\n|:----------------------|:-----------|\n| Bearer authentication | All endpoints |\n| Token authentication | v1 and v2 compatibility endpoints (`/write`, `/query`, `/api/v2/write`) |\n| Basic authentication | v1 compatibility endpoints (`/write`, `/query`) |\n| Querystring authentication | v1 compatibility endpoints (`/write`, `/query`) |\n\nSee the **Security Schemes** section below for details on each authentication method.\n", "source": "static/openapi/influxdb3-core/tags/tags/ref-authentication.yaml", "staticFilePath": "/openapi/influxdb3-core/tags/tags/ref-authentication.yaml" } diff --git a/data/article_data/influxdb/influxdb3_core/articles.yml b/data/article_data/influxdb/influxdb3_core/articles.yml index c8f317c95..b68eb76ea 100644 --- a/data/article_data/influxdb/influxdb3_core/articles.yml +++ b/data/article_data/influxdb/influxdb3_core/articles.yml @@ -44,7 +44,6 @@ articles: fields: name: Authentication describes: - - /api/v3/configure/token/admin - /api/v3/configure/token/admin/regenerate - /api/v3/configure/token - /api/v3/configure/token/named_admin @@ -60,11 +59,14 @@ articles: | Bearer authentication | All endpoints | - | Token authentication | v1, v2 endpoints | + | Token authentication | v1 and v2 compatibility endpoints (`/write`, + `/query`, `/api/v2/write`) | - | Basic authentication | v1 endpoints | + | Basic authentication | v1 compatibility endpoints (`/write`, + `/query`) | - | Querystring authentication | v1 endpoints | + | Querystring authentication | v1 compatibility endpoints (`/write`, + `/query`) | See the **Security Schemes** section below for details on each @@ -73,12 +75,6 @@ articles: isConceptual: true menuGroup: Concepts operations: - - operationId: PostCreateAdminToken - method: POST - path: /api/v3/configure/token/admin - summary: Create admin token - tags: - - Authentication - operationId: PostRegenerateAdminToken method: POST path: /api/v3/configure/token/admin/regenerate @@ -108,11 +104,14 @@ articles: | Bearer authentication | All endpoints | - | Token authentication | v1, v2 endpoints | + | Token authentication | v1 and v2 compatibility endpoints (`/write`, + `/query`, `/api/v2/write`) | - | Basic authentication | v1 endpoints | + | Basic authentication | v1 compatibility endpoints (`/write`, + `/query`) | - | Querystring authentication | v1 endpoints | + | Querystring authentication | v1 compatibility endpoints (`/write`, + `/query`) | See the **Security Schemes** section below for details on each diff --git a/data/article_data/influxdb/influxdb3_enterprise/articles.json b/data/article_data/influxdb/influxdb3_enterprise/articles.json index 7d9b658d4..0e49c9cb8 100644 --- a/data/article_data/influxdb/influxdb3_enterprise/articles.json +++ b/data/article_data/influxdb/influxdb3_enterprise/articles.json @@ -74,13 +74,12 @@ "name": "Authentication", "describes": [ "/api/v3/configure/enterprise/token", - "/api/v3/configure/token/admin", "/api/v3/configure/token/admin/regenerate", "/api/v3/configure/token", "/api/v3/configure/token/named_admin" ], "title": "Authentication", - "description": "Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API:\n| Authentication scheme | Works with |\n|:----------------------|:-----------|\n| Bearer authentication | All endpoints |\n| Token authentication | v1, v2 endpoints |\n| Basic authentication | v1 endpoints |\n| Querystring authentication | v1 endpoints |\nSee the **Security Schemes** section below for details on each authentication method.\n", + "description": "Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API:\n| Authentication scheme | Works with |\n|:----------------------|:-----------|\n| Bearer authentication | All endpoints |\n| Token authentication | v1 and v2 compatibility endpoints (`/write`, `/query`, `/api/v2/write`) |\n| Basic authentication | v1 compatibility endpoints (`/write`, `/query`) |\n| Querystring authentication | v1 compatibility endpoints (`/write`, `/query`) |\nSee the **Security Schemes** section below for details on each authentication method.\n", "tag": "Authentication", "isConceptual": true, "menuGroup": "Concepts", @@ -94,15 +93,6 @@ "Authentication" ] }, - { - "operationId": "PostCreateAdminToken", - "method": "POST", - "path": "/api/v3/configure/token/admin", - "summary": "Create admin token", - "tags": [ - "Authentication" - ] - }, { "operationId": "PostRegenerateAdminToken", "method": "POST", @@ -131,7 +121,7 @@ ] } ], - "tagDescription": "Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API:\n| Authentication scheme | Works with |\n|:----------------------|:-----------|\n| Bearer authentication | All endpoints |\n| Token authentication | v1, v2 endpoints |\n| Basic authentication | v1 endpoints |\n| Querystring authentication | v1 endpoints |\nSee the **Security Schemes** section below for details on each authentication method.\n", + "tagDescription": "Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API:\n| Authentication scheme | Works with |\n|:----------------------|:-----------|\n| Bearer authentication | All endpoints |\n| Token authentication | v1 and v2 compatibility endpoints (`/write`, `/query`, `/api/v2/write`) |\n| Basic authentication | v1 compatibility endpoints (`/write`, `/query`) |\n| Querystring authentication | v1 compatibility endpoints (`/write`, `/query`) |\nSee the **Security Schemes** section below for details on each authentication method.\n", "source": "static/openapi/influxdb3-enterprise/tags/tags/ref-authentication.yaml", "staticFilePath": "/openapi/influxdb3-enterprise/tags/tags/ref-authentication.yaml" } diff --git a/data/article_data/influxdb/influxdb3_enterprise/articles.yml b/data/article_data/influxdb/influxdb3_enterprise/articles.yml index acc7d4262..3e99f708f 100644 --- a/data/article_data/influxdb/influxdb3_enterprise/articles.yml +++ b/data/article_data/influxdb/influxdb3_enterprise/articles.yml @@ -52,7 +52,6 @@ articles: name: Authentication describes: - /api/v3/configure/enterprise/token - - /api/v3/configure/token/admin - /api/v3/configure/token/admin/regenerate - /api/v3/configure/token - /api/v3/configure/token/named_admin @@ -67,11 +66,14 @@ articles: | Bearer authentication | All endpoints | - | Token authentication | v1, v2 endpoints | + | Token authentication | v1 and v2 compatibility endpoints (`/write`, + `/query`, `/api/v2/write`) | - | Basic authentication | v1 endpoints | + | Basic authentication | v1 compatibility endpoints (`/write`, + `/query`) | - | Querystring authentication | v1 endpoints | + | Querystring authentication | v1 compatibility endpoints (`/write`, + `/query`) | See the **Security Schemes** section below for details on each authentication method. @@ -85,12 +87,6 @@ articles: summary: Create a resource token tags: - Authentication - - operationId: PostCreateAdminToken - method: POST - path: /api/v3/configure/token/admin - summary: Create admin token - tags: - - Authentication - operationId: PostRegenerateAdminToken method: POST path: /api/v3/configure/token/admin/regenerate @@ -119,11 +115,14 @@ articles: | Bearer authentication | All endpoints | - | Token authentication | v1, v2 endpoints | + | Token authentication | v1 and v2 compatibility endpoints (`/write`, + `/query`, `/api/v2/write`) | - | Basic authentication | v1 endpoints | + | Basic authentication | v1 compatibility endpoints (`/write`, + `/query`) | - | Querystring authentication | v1 endpoints | + | Querystring authentication | v1 compatibility endpoints (`/write`, + `/query`) | See the **Security Schemes** section below for details on each authentication method. diff --git a/layouts/partials/api/rapidoc-mini.html b/layouts/partials/api/rapidoc-mini.html index 20831f8cd..4ec33c568 100644 --- a/layouts/partials/api/rapidoc-mini.html +++ b/layouts/partials/api/rapidoc-mini.html @@ -10,11 +10,30 @@ Optional page params: - operationId: Operation ID for linking/navigation purposes + - apiPath: The API path (used to determine auth schemes) */}} {{ $specPath := .Params.specFile }} {{ $specPathJSON := replace $specPath ".yaml" ".json" | replace ".yml" ".json" }} {{ $matchPaths := .Params.matchPaths | default "" }} +{{ $operationId := .Params.operationId | default "" }} +{{ $title := .Title | default "" }} +{{ $apiPath := .Params.apiPath | default "" }} + +{{/* + Determine supported auth schemes based on API path: + - /api/v3/* endpoints: BearerAuthentication only + - /api/v2/* endpoints: BearerAuthentication + TokenAuthentication + - /write, /query (v1): All 4 schemes (Bearer, Token, Basic, Querystring) +*/}} +{{ $authSchemes := "bearer" }} +{{ if hasPrefix $apiPath "/api/v3" }} + {{ $authSchemes = "bearer" }} +{{ else if hasPrefix $apiPath "/api/v2" }} + {{ $authSchemes = "bearer,token" }} +{{ else if or (eq $apiPath "/write") (eq $apiPath "/query") }} + {{ $authSchemes = "bearer,token,basic,querystring" }} +{{ end }} {{/* Machine-readable links for AI agent discovery */}} {{ if $specPath }} @@ -22,17 +41,34 @@ {{ end }} -{{/* Auth input component */}} -
+{{/* Auth credentials popover trigger - positioned above the operation */}} +
+ + {{/* Popover container - positioned by CSS */}} +
{{/* Component container - TypeScript handles initialization */}}
+ {{ with $matchPaths }}data-match-paths="{{ . }}"{{ end }} + {{ with $operationId }}data-operation-id="{{ . }}"{{ end }} + {{ with $title }}data-title="{{ . }}"{{ end }}> {{/* RapiDoc Mini element created by TypeScript component */}}
@@ -71,11 +107,14 @@ rapi-doc { border: none; border-radius: 0; - /* Override RapiDoc internal CSS variables - subtle borders */ + /* Override RapiDoc internal CSS variables */ --light-border-color: rgba(0, 163, 255, 0.25); --border-color: rgba(0, 163, 255, 0.25); - /* HTTP method colors - lighter palette for light theme */ - --blue: #00A3FF; /* $b-pool - GET */ + --font-size-regular: 16px; + + /* Use green for links/status text so they don't look like clickable links */ + /* HTTP method colors are set via element attributes, not these variables */ + --blue: #34BB55; /* $gr-rainforest - green for status/links */ --green: #34BB55; /* $gr-rainforest - POST */ --orange: #FFB94A; /* $y-pineapple - PUT (distinct from red) */ --red: #F95F53; /* $r-curacao - DELETE */ @@ -87,8 +126,8 @@ rapi-doc { [data-theme="dark"] rapi-doc, html:has(link[title="dark-theme"]:not([disabled])) rapi-doc-mini, html:has(link[title="dark-theme"]:not([disabled])) rapi-doc { - /* HTTP method colors - darker palette for dark theme */ - --blue: #066FC5; /* $b-ocean - GET */ + /* Use green for links/status text in dark mode */ + --blue: #009F5F; /* $gr-viridian - green for status/links */ --green: #009F5F; /* $gr-viridian - POST */ --orange: #FFC800; /* $y-thunder - PUT (brighter for dark mode) */ --red: #DC4E58; /* $r-fire - DELETE */ @@ -100,12 +139,6 @@ rapi-doc::part(section-tag) { display: none; } -/* Fix parameter table layout - reduce indentation from empty td cells */ -/* Match site's body font size (16px) for consistency */ -rapi-doc { - --font-size-regular: 16px; -} - /* Fix auth schemes at narrow widths - ensure content is scrollable */ @media (max-width: 1280px) { .api-reference-mini {