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
parent
9df709851b
commit
6c25072ed3
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue