feat(api): Convert auth input to popover with filtered schemes

- Convert collapsible auth panel to popover UI triggered by
  "Set credentials" button positioned above RapiDoc
- Filter auth schemes based on API path:
  - /api/v3/* endpoints: Bearer only
  - /api/v2/* endpoints: Bearer + Token
  - /write, /query (v1): All 4 schemes
- Fix "Authentication Not Required" bug by adding global security
  field to path-specific OpenAPI specs
- Change RapiDoc --blue CSS variable to green so status text and
  type links don't look like clickable links
- Add explicit button styling for Apply/Clear in popover
- Hide RapiDoc's built-in auth section (use custom popover instead)
claude/api-code-samples-plan-MEkQO
Jason Stirnaman 2025-12-22 13:24:18 -06:00
parent 9df709851b
commit 6c25072ed3
12 changed files with 1281 additions and 193 deletions

View File

@ -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)

View File

@ -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

View File

@ -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<string, string>;
}
/**
@ -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<string, string>;
}
/**
@ -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<string, unknown> = {
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)

View File

@ -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<string, string> {
const pathSpecFiles = new Map<string, string>();
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<string>();
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<string, string> {
const openapi = readFile(specFile, 'utf8');
return writePathSpecificSpecs(openapi, outPath);
}
// CommonJS export for backward compatibility
module.exports = {
generateHugoData,
generateHugoDataByTag,
generatePathSpecificSpecs,
writePathSpecificSpecs,
};

View File

@ -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:
* <button data-component="api-auth-input"
* data-schemes="bearer,token"
* data-popover="true">
* Set credentials
* </button>
* <div class="api-auth-popover" hidden></div>
*/
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 = `
<h4>Authentication</h4>
<p class="auth-form-description">Enter credentials to use with "Try it" requests.</p>
${
schemes.includes('bearer')
? `
<div class="auth-field">
<label for="auth-bearer">Bearer Token</label>
<input type="password" id="auth-bearer" placeholder="Enter your API token" />
</div>
`
: ''
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<HTMLInputElement>(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')
? `
<div class="auth-field">
<label for="auth-username">Username</label>
<input type="text" id="auth-username" placeholder="Username (optional)" />
</div>
<div class="auth-field">
<label for="auth-password">Password / Token</label>
<input type="password" id="auth-password" placeholder="Enter token" />
</div>
`
: ''
} 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);
}
<button type="button" class="btn btn-primary auth-save">Save Credentials</button>
}
// 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 `
<div class="auth-field" data-scheme="bearer">
<label for="auth-bearer">
<span class="auth-label-text">API Token</span>
<span class="auth-label-hint">(Bearer auth)</span>
</label>
<div class="auth-input-group">
<input type="password"
id="auth-bearer"
placeholder="Enter your API token"
autocomplete="off" />
<button type="button" class="auth-show-toggle" data-target="auth-bearer" aria-label="Show token">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 3C4.5 3 1.5 5.5 0 8c1.5 2.5 4.5 5 8 5s6.5-2.5 8-5c-1.5-2.5-4.5-5-8-5zm0 8c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/>
</svg>
</button>
</div>
</div>`;
case 'token':
return `
<div class="auth-field" data-scheme="token">
<label for="auth-token">
<span class="auth-label-text">API Token</span>
<span class="auth-label-hint">(v2 Token scheme)</span>
</label>
<div class="auth-input-group">
<input type="password"
id="auth-token"
placeholder="Enter your API token"
autocomplete="off" />
<button type="button" class="auth-show-toggle" data-target="auth-token" aria-label="Show token">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 3C4.5 3 1.5 5.5 0 8c1.5 2.5 4.5 5 8 5s6.5-2.5 8-5c-1.5-2.5-4.5-5-8-5zm0 8c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/>
</svg>
</button>
</div>
</div>`;
case 'basic':
return `
<div class="auth-field-group" data-scheme="basic">
<p class="auth-group-label">Basic Authentication <span class="auth-label-hint">(v1 compatibility)</span></p>
<div class="auth-field">
<label for="auth-username">Username</label>
<input type="text"
id="auth-username"
placeholder="Optional"
autocomplete="username" />
</div>
<div class="auth-field">
<label for="auth-password">Password / Token</label>
<div class="auth-input-group">
<input type="password"
id="auth-password"
placeholder="Enter token"
autocomplete="current-password" />
<button type="button" class="auth-show-toggle" data-target="auth-password" aria-label="Show password">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 3C4.5 3 1.5 5.5 0 8c1.5 2.5 4.5 5 8 5s6.5-2.5 8-5c-1.5-2.5-4.5-5-8-5zm0 8c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/>
</svg>
</button>
</div>
</div>
</div>`;
case 'querystring':
return `
<div class="auth-field" data-scheme="querystring">
<label for="auth-querystring">
<span class="auth-label-text">Query Parameter Token</span>
<span class="auth-label-hint">(v1 ?p=TOKEN)</span>
</label>
<div class="auth-input-group">
<input type="password"
id="auth-querystring"
placeholder="Enter token for ?p= parameter"
autocomplete="off" />
<button type="button" class="auth-show-toggle" data-target="auth-querystring" aria-label="Show token">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 3C4.5 3 1.5 5.5 0 8c1.5 2.5 4.5 5 8 5s6.5-2.5 8-5c-1.5-2.5-4.5-5-8-5zm0 8c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/>
</svg>
</button>
</div>
</div>`;
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 `
<div class="api-auth-popover-content">
<div class="popover-header">
<h4>API Credentials</h4>
<button type="button" class="popover-close" aria-label="Close">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
</div>
<p class="auth-description">
Enter credentials for "Try it" requests.
</p>
${fields.replace('(Bearer auth)', bearerLabel)}
<div class="auth-actions">
<button type="button" class="btn btn-sm auth-apply">Apply</button>
<button type="button" class="btn btn-sm btn-secondary auth-clear">Clear</button>
</div>
<p class="auth-feedback" hidden></p>
</div>
`;
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<HTMLElement>('.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<HTMLElement>(
'.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<HTMLInputElement>('#auth-bearer');
const usernameInput = form.querySelector<HTMLInputElement>('#auth-username');
const passwordInput = form.querySelector<HTMLInputElement>('#auth-password');
// Element references
const bearerInput = popover.querySelector<HTMLInputElement>('#auth-bearer');
const tokenInput = popover.querySelector<HTMLInputElement>('#auth-token');
const usernameInput =
popover.querySelector<HTMLInputElement>('#auth-username');
const passwordInput =
popover.querySelector<HTMLInputElement>('#auth-password');
const querystringInput =
popover.querySelector<HTMLInputElement>('#auth-querystring');
const applyBtn = popover.querySelector<HTMLButtonElement>('.auth-apply');
const clearBtn = popover.querySelector<HTMLButtonElement>('.auth-clear');
const closeBtn = popover.querySelector<HTMLButtonElement>('.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<HTMLInputElement>(
'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<HTMLButtonElement>('.auth-show-toggle');
showToggles.forEach((btn) => {
btn.addEventListener('click', () => {
const targetId = btn.dataset.target;
const input = popover.querySelector<HTMLInputElement>(`#${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);
};
}

View File

@ -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

View File

@ -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;
}
}
}
}

View File

@ -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"
}

View File

@ -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

View File

@ -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"
}

View File

@ -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.

View File

@ -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 @@
<link rel="alternate" type="application/json" href="{{ $specPathJSON | absURL }}" title="OpenAPI Specification (JSON)" />
{{ end }}
{{/* Auth input component */}}
<div class="api-auth-input-wrapper"
data-component="api-auth-input"
data-schemes="bearer,basic">
{{/* Auth credentials popover trigger - positioned above the operation */}}
<div class="api-auth-trigger-wrapper">
<button type="button"
class="api-auth-trigger btn btn-sm"
data-component="api-auth-input"
data-schemes="{{ $authSchemes }}"
data-popover="true"
aria-expanded="false"
aria-haspopup="dialog">
<svg class="auth-icon" width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V6H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-1.5V4.5A3.5 3.5 0 0 0 8 1zm2 5H6V4.5a2 2 0 1 1 4 0V6z"/>
</svg>
<span class="auth-trigger-text">Set credentials</span>
<span class="auth-status-indicator" hidden aria-label="Credentials configured"></span>
</button>
{{/* Popover container - positioned by CSS */}}
<div class="api-auth-popover" role="dialog" aria-label="API credentials" hidden>
{{/* Content rendered by TypeScript component */}}
</div>
</div>
{{/* Component container - TypeScript handles initialization */}}
<div class="api-reference-wrapper api-reference-mini"
data-component="rapidoc-mini"
data-spec-url="{{ $specPath }}"
{{ with $matchPaths }}data-match-paths="{{ . }}"{{ end }}>
{{ with $matchPaths }}data-match-paths="{{ . }}"{{ end }}
{{ with $operationId }}data-operation-id="{{ . }}"{{ end }}
{{ with $title }}data-title="{{ . }}"{{ end }}>
{{/* RapiDoc Mini element created by TypeScript component */}}
</div>
@ -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 {