mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
[Docs][API] - Updates our Docs to allow for project selection on api pages (#1058)
<!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> Updates the Auth panel on API pages to allow for authenticated users to select a project from project drop downs. This enables easy access for the user to select their project, and test endpoints against it. <img width="399" height="521" alt="image" src="https://github.com/user-attachments/assets/0d3a8444-2b69-4a21-b0ce-ce3515c4672d" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Project selection UI for configuring admin-scoped API access * Automatic admin-token refresh and auto-population of admin headers from the signed-in user * **Improvements** * Enhanced header management (supports functional updates and clearer handling) * Automatic token refresh before requests and improved base URL resolution across environments * Default project fallback when no project is configured * **UX** * clearer admin-related indicators, read-only styling, and contextual messaging for auto-populated headers <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
b6180d5912
commit
ddcab1bb8b
@ -1,16 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
import { createContext, ReactNode, useCallback, useContext, useState } from 'react';
|
||||
import { useSidebar } from '../layouts/sidebar-context';
|
||||
|
||||
// Stack Auth required headers
|
||||
// Note: Content-Type is NOT included here - it's added automatically by fetch when there's a body
|
||||
const STACK_AUTH_HEADERS = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Stack-Access-Type': '', // client or server
|
||||
'X-Stack-Access-Type': '', // client, server, or admin
|
||||
'X-Stack-Project-Id': '', // project UUID
|
||||
'X-Stack-Publishable-Client-Key': '', // pck_...
|
||||
'X-Stack-Secret-Server-Key': '', // ssk_...
|
||||
'X-Stack-Access-Token': '', // user's access token
|
||||
'X-Stack-Admin-Access-Token': '', // admin access token (for owned projects)
|
||||
};
|
||||
|
||||
// Type for API error objects
|
||||
@ -23,9 +24,11 @@ type APIError = {
|
||||
}
|
||||
|
||||
// Context for sharing headers across all API components on the page
|
||||
type UpdateSharedHeadersInput = Record<string, string> | ((current: Record<string, string>) => Record<string, string>);
|
||||
|
||||
type APIPageContextType = {
|
||||
sharedHeaders: Record<string, string>,
|
||||
updateSharedHeaders: (headers: Record<string, string>) => void,
|
||||
updateSharedHeaders: (headers: UpdateSharedHeadersInput) => void,
|
||||
reportError: (status: number, error: APIError) => void,
|
||||
lastError: { status: number, error: APIError } | null,
|
||||
highlightMissingHeaders: boolean,
|
||||
@ -57,13 +60,15 @@ export function APIPageWrapper({ children }: APIPageWrapperProps) {
|
||||
toggleAuth: () => {}
|
||||
};
|
||||
|
||||
const updateSharedHeaders = (headers: Record<string, string>) => {
|
||||
setSharedHeaders(headers);
|
||||
// Clear error highlighting when headers are updated
|
||||
if (highlightMissingHeaders) {
|
||||
setHighlightMissingHeaders(false);
|
||||
}
|
||||
};
|
||||
const updateSharedHeaders = useCallback((headers: UpdateSharedHeadersInput) => {
|
||||
setSharedHeaders(prevHeaders => {
|
||||
if (typeof headers === 'function') {
|
||||
return headers(prevHeaders);
|
||||
}
|
||||
return headers;
|
||||
});
|
||||
setHighlightMissingHeaders(false);
|
||||
}, []);
|
||||
|
||||
const reportError = (status: number, error: APIError) => {
|
||||
setLastError({ status, error });
|
||||
|
||||
@ -1,17 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { AlertTriangle, Key, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AdminOwnedProject, CurrentInternalUser, useUser } from '@stackframe/stack';
|
||||
import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises';
|
||||
import { stringCompare } from '@stackframe/stack-shared/dist/utils/strings';
|
||||
import { AlertTriangle, ChevronDown, Key, X } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSidebar } from '../layouts/sidebar-context';
|
||||
import { useAPIPageContext } from './api-page-wrapper';
|
||||
import { Button } from './button';
|
||||
|
||||
type StackAuthHeaderKey =
|
||||
| 'X-Stack-Access-Type'
|
||||
| 'X-Stack-Project-Id'
|
||||
| 'X-Stack-Publishable-Client-Key'
|
||||
| 'X-Stack-Secret-Server-Key'
|
||||
| 'X-Stack-Access-Token'
|
||||
| 'X-Stack-Admin-Access-Token';
|
||||
|
||||
type StackAuthHeaderField = {
|
||||
key: StackAuthHeaderKey,
|
||||
label: string,
|
||||
placeholder: string,
|
||||
required: boolean,
|
||||
hideWhenProjectSelected?: boolean,
|
||||
isSensitive?: boolean,
|
||||
};
|
||||
|
||||
const stackAuthHeaders: StackAuthHeaderField[] = [
|
||||
{ key: 'X-Stack-Access-Type', label: 'Access Type', placeholder: 'client, server, or admin', required: true },
|
||||
{ key: 'X-Stack-Project-Id', label: 'Project ID', placeholder: 'your-project-uuid', required: true },
|
||||
{ key: 'X-Stack-Publishable-Client-Key', label: 'Client Key', placeholder: 'pck_your_key_here', required: false, hideWhenProjectSelected: true },
|
||||
{ key: 'X-Stack-Secret-Server-Key', label: 'Server Key', placeholder: 'ssk_your_key_here', required: false, hideWhenProjectSelected: true },
|
||||
{ key: 'X-Stack-Access-Token', label: 'Access Token', placeholder: 'user_access_token', required: false, hideWhenProjectSelected: true },
|
||||
{ key: 'X-Stack-Admin-Access-Token', label: 'Admin Access Token', placeholder: 'admin_access_token', required: false, isSensitive: true },
|
||||
];
|
||||
|
||||
type UserHookResult = ReturnType<typeof useUser>;
|
||||
|
||||
function isInternalUser(user: UserHookResult): user is CurrentInternalUser {
|
||||
return Boolean(user && 'useOwnedProjects' in user && typeof user.useOwnedProjects === 'function');
|
||||
}
|
||||
|
||||
export function AuthPanel() {
|
||||
const sidebarContext = useSidebar();
|
||||
|
||||
// Always call hooks at the top level
|
||||
const apiContext = useAPIPageContext();
|
||||
|
||||
// Get current user and their projects
|
||||
// Docs and dashboard share the same authentication (internal project)
|
||||
// So logged-in users will have access to their owned projects via useOwnedProjects()
|
||||
const user = useUser();
|
||||
const internalUser = isInternalUser(user) ? user : null;
|
||||
const ownedProjectsResult = internalUser?.useOwnedProjects();
|
||||
const projects = useMemo<AdminOwnedProject[]>(() => ownedProjectsResult ?? [], [ownedProjectsResult]);
|
||||
const hasOwnedProjects = Boolean(internalUser);
|
||||
|
||||
// State for project selection
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string>('');
|
||||
|
||||
// Use default functions if sidebar context is not available
|
||||
const { isAuthOpen, toggleAuth } = sidebarContext || {
|
||||
isAuthOpen: false,
|
||||
@ -19,13 +66,14 @@ export function AuthPanel() {
|
||||
};
|
||||
|
||||
// Default headers structure
|
||||
const defaultHeaders: Record<string, string> = {
|
||||
'Content-Type': '',
|
||||
// Note: Content-Type is handled automatically by the request handler when there's a body
|
||||
const defaultHeaders: Record<StackAuthHeaderKey, string> = {
|
||||
'X-Stack-Access-Type': '',
|
||||
'X-Stack-Project-Id': '',
|
||||
'X-Stack-Publishable-Client-Key': '',
|
||||
'X-Stack-Secret-Server-Key': '',
|
||||
'X-Stack-Access-Token': '',
|
||||
'X-Stack-Admin-Access-Token': '',
|
||||
};
|
||||
|
||||
const { sharedHeaders, updateSharedHeaders, lastError, highlightMissingHeaders } = apiContext || {
|
||||
@ -38,6 +86,26 @@ export function AuthPanel() {
|
||||
// Ensure sharedHeaders is always a Record<string, string>
|
||||
const headers: Record<string, string> = sharedHeaders;
|
||||
|
||||
// Refresh admin access token when project is selected
|
||||
useEffect(() => {
|
||||
if (!selectedProjectId || !user) {
|
||||
return;
|
||||
}
|
||||
|
||||
runAsynchronously(async () => {
|
||||
// Get fresh access token from user's session
|
||||
const authJson = await user.getAuthJson();
|
||||
const adminAccessToken = authJson.accessToken ?? '';
|
||||
if (adminAccessToken) {
|
||||
// Update only the admin access token in headers
|
||||
updateSharedHeaders(prevHeaders => ({
|
||||
...prevHeaders,
|
||||
'X-Stack-Admin-Access-Token': adminAccessToken,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, [selectedProjectId, user, updateSharedHeaders]);
|
||||
|
||||
const [isHomePage, setIsHomePage] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
@ -75,19 +143,37 @@ export function AuthPanel() {
|
||||
const topPosition = isHomePage && isScrolled ? 'top-0' : 'top-0';
|
||||
const height = isHomePage && isScrolled ? 'h-screen' : 'h-[calc(100vh)]';
|
||||
|
||||
const stackAuthHeaders = [
|
||||
{ key: 'Content-Type', label: 'Content Type', placeholder: 'application/json', required: true },
|
||||
{ key: 'X-Stack-Access-Type', label: 'Access Type', placeholder: 'client or server', required: true },
|
||||
{ key: 'X-Stack-Project-Id', label: 'Project ID', placeholder: 'your-project-uuid', required: true },
|
||||
{ key: 'X-Stack-Publishable-Client-Key', label: 'Client Key', placeholder: 'pck_your_key_here', required: false },
|
||||
{ key: 'X-Stack-Secret-Server-Key', label: 'Server Key', placeholder: 'ssk_your_key_here', required: false },
|
||||
{ key: 'X-Stack-Access-Token', label: 'Access Token', placeholder: 'user_access_token', required: false },
|
||||
];
|
||||
|
||||
const missingRequiredHeaders = stackAuthHeaders.filter(
|
||||
header => header.required && !headers[header.key].trim()
|
||||
header => header.required && !(headers[header.key] ?? '').trim()
|
||||
);
|
||||
|
||||
// Handle project selection
|
||||
const handleProjectSelect = (projectId: string) => {
|
||||
if (!projects.some((projectItem) => projectItem.id === projectId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedProjectId(projectId);
|
||||
|
||||
// Initial headers setup - token will be populated by the useEffect
|
||||
const newHeaders = {
|
||||
...headers,
|
||||
'X-Stack-Access-Type': 'admin',
|
||||
'X-Stack-Project-Id': projectId,
|
||||
'X-Stack-Admin-Access-Token': '', // Will be populated by refresh effect
|
||||
'X-Stack-Access-Token': '', // Not used for admin access
|
||||
'X-Stack-Publishable-Client-Key': '', // Not needed for admin auth
|
||||
'X-Stack-Secret-Server-Key': '', // Not needed for admin auth
|
||||
};
|
||||
|
||||
updateSharedHeaders(newHeaders);
|
||||
};
|
||||
|
||||
// Sort projects by name for better UX
|
||||
const sortedProjects = useMemo(() => {
|
||||
return [...projects].sort((a, b) => stringCompare(a.displayName, b.displayName));
|
||||
}, [projects]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop Auth Panel - Matching AIChatDrawer design */}
|
||||
@ -150,8 +236,48 @@ export function AuthPanel() {
|
||||
{/* Content - Fixed height to prevent layout shifts */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="h-full overflow-y-auto p-3 space-y-3">
|
||||
{/* Project Selector - Show only if user has owned projects */}
|
||||
{hasOwnedProjects && projects.length > 0 && (
|
||||
<div className="space-y-1.5 pb-3 border-b border-fd-border">
|
||||
<label className="text-xs font-medium text-fd-foreground flex items-center gap-2">
|
||||
Quick Select Project
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||
logged in
|
||||
</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedProjectId}
|
||||
onChange={(e) => handleProjectSelect(e.target.value)}
|
||||
className="w-full px-2 py-1.5 pr-8 border rounded-md text-xs bg-fd-background text-fd-foreground focus:outline-none focus:ring-1 focus:ring-fd-primary focus:border-fd-primary border-fd-border appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="">Select a project...</option>
|
||||
{sortedProjects.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.displayName} ({project.id.slice(0, 8)}...)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 text-fd-muted-foreground pointer-events-none" />
|
||||
</div>
|
||||
{selectedProjectId && (
|
||||
<p className="text-xs text-green-600 dark:text-green-400">
|
||||
✓ Headers auto-populated for admin authentication
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manual Header Inputs */}
|
||||
{stackAuthHeaders.map((header) => {
|
||||
const isMissing = highlightMissingHeaders && header.required && !headers[header.key].trim();
|
||||
// Hide certain fields when project is selected
|
||||
if (selectedProjectId && header.hideWhenProjectSelected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = headers[header.key] ?? '';
|
||||
const isMissing = highlightMissingHeaders && header.required && !value.trim();
|
||||
const isAutoPopulated = Boolean(header.isSensitive && selectedProjectId && value.length > 0);
|
||||
|
||||
return (
|
||||
<div key={header.key} className={`space-y-1.5 ${
|
||||
@ -175,14 +301,18 @@ export function AuthPanel() {
|
||||
<input
|
||||
type="text"
|
||||
placeholder={header.placeholder}
|
||||
value={headers[header.key] || ''}
|
||||
value={value}
|
||||
onChange={(e) => updateSharedHeaders({ ...headers, [header.key]: e.target.value })}
|
||||
readOnly={isAutoPopulated}
|
||||
className={`w-full px-2 py-1.5 border rounded-md text-xs bg-fd-background text-fd-foreground placeholder:text-fd-muted-foreground focus:outline-none focus:ring-1 focus:border-transparent transition-all duration-200 ${
|
||||
isMissing
|
||||
? 'border-red-300 focus:ring-red-500 dark:border-red-700'
|
||||
: 'border-fd-border focus:ring-fd-primary focus:border-fd-primary'
|
||||
}`}
|
||||
} ${isAutoPopulated ? 'bg-fd-muted/50 cursor-not-allowed' : ''}`}
|
||||
/>
|
||||
{isAutoPopulated && (
|
||||
<p className="text-xs text-fd-muted-foreground">Auto-populated from your account</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -196,7 +326,8 @@ export function AuthPanel() {
|
||||
missingRequiredHeaders.length === 0 ? 'bg-green-500' : 'bg-red-500 auth-error-pulse'
|
||||
}`} />
|
||||
<span className="text-fd-muted-foreground">
|
||||
{Object.values(headers).filter(v => v.trim()).length} of {stackAuthHeaders.length} configured
|
||||
{Object.values(headers).filter(v => v.trim()).length} configured
|
||||
{selectedProjectId && ' (via project selection)'}
|
||||
</span>
|
||||
</div>
|
||||
{missingRequiredHeaders.length === 0 && Object.values(headers).some(v => v.trim()) && (
|
||||
@ -271,8 +402,48 @@ export function AuthPanel() {
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="h-full overflow-y-auto p-3">
|
||||
<div className="space-y-3">
|
||||
{/* Project Selector - Mobile */}
|
||||
{hasOwnedProjects && projects.length > 0 && (
|
||||
<div className="space-y-1.5 pb-3 border-b border-fd-border">
|
||||
<label className="text-sm font-medium text-fd-foreground flex items-center gap-2">
|
||||
Quick Select Project
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||
logged in
|
||||
</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedProjectId}
|
||||
onChange={(e) => handleProjectSelect(e.target.value)}
|
||||
className="w-full px-3 py-2 pr-8 border rounded-md text-sm bg-fd-background text-fd-foreground focus:outline-none focus:ring-1 focus:ring-fd-primary focus:border-fd-primary border-fd-border appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="">Select a project...</option>
|
||||
{sortedProjects.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.displayName} ({project.id.slice(0, 8)}...)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-fd-muted-foreground pointer-events-none" />
|
||||
</div>
|
||||
{selectedProjectId && (
|
||||
<p className="text-xs text-green-600 dark:text-green-400">
|
||||
✓ Headers auto-populated for admin authentication
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manual Header Inputs - Mobile */}
|
||||
{stackAuthHeaders.map((header) => {
|
||||
const isMissing = highlightMissingHeaders && header.required && !headers[header.key].trim();
|
||||
// Hide certain fields when project is selected
|
||||
if (selectedProjectId && header.hideWhenProjectSelected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = headers[header.key] ?? '';
|
||||
const isMissing = highlightMissingHeaders && header.required && !value.trim();
|
||||
const isAutoPopulated = Boolean(header.isSensitive && selectedProjectId && value.length > 0);
|
||||
|
||||
return (
|
||||
<div key={header.key} className={`space-y-1.5 ${
|
||||
@ -296,14 +467,18 @@ export function AuthPanel() {
|
||||
<input
|
||||
type="text"
|
||||
placeholder={header.placeholder}
|
||||
value={headers[header.key] || ''}
|
||||
value={value}
|
||||
onChange={(e) => updateSharedHeaders({ ...headers, [header.key]: e.target.value })}
|
||||
readOnly={isAutoPopulated}
|
||||
className={`w-full px-3 py-2 border rounded-md text-sm bg-fd-background text-fd-foreground placeholder:text-fd-muted-foreground focus:outline-none focus:ring-1 focus:border-transparent transition-all duration-200 ${
|
||||
isMissing
|
||||
? 'border-red-300 focus:ring-red-500 dark:border-red-700'
|
||||
: 'border-fd-border focus:ring-fd-primary focus:border-fd-primary'
|
||||
}`}
|
||||
} ${isAutoPopulated ? 'bg-fd-muted/50 cursor-not-allowed' : ''}`}
|
||||
/>
|
||||
{isAutoPopulated && (
|
||||
<p className="text-xs text-fd-muted-foreground">Auto-populated from your account</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -319,7 +494,8 @@ export function AuthPanel() {
|
||||
missingRequiredHeaders.length === 0 ? 'bg-green-500' : 'bg-red-500 auth-error-pulse'
|
||||
}`} />
|
||||
<span className="text-fd-muted-foreground">
|
||||
{Object.values(headers).filter(v => v.trim()).length} of {stackAuthHeaders.length} configured
|
||||
{Object.values(headers).filter(v => v.trim()).length} configured
|
||||
{selectedProjectId && ' (via project)'}
|
||||
</span>
|
||||
</div>
|
||||
<Button onClick={toggleAuth} className="text-xs px-3 py-1">
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useUser } from '@stackframe/stack';
|
||||
import { ArrowRight, Check, Code, Copy, Play, Send, Settings, Zap } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { OpenAPIOperation, OpenAPIParameter, OpenAPISchema, OpenAPISpec } from '../../lib/openapi-types';
|
||||
@ -42,11 +43,13 @@ const HTTP_METHOD_COLORS = {
|
||||
|
||||
export function EnhancedAPIPage({ document, operations, description }: EnhancedAPIPageProps) {
|
||||
const apiContext = useAPIPageContext();
|
||||
const user = useUser(); // Get user for token refresh
|
||||
|
||||
// Use default functions if API context is not available
|
||||
const { sharedHeaders, reportError } = apiContext || {
|
||||
const { sharedHeaders, reportError, updateSharedHeaders } = apiContext || {
|
||||
sharedHeaders: {},
|
||||
reportError: () => {}
|
||||
reportError: () => {},
|
||||
updateSharedHeaders: () => {}
|
||||
};
|
||||
|
||||
const [spec, setSpec] = useState<OpenAPISpec | null>(null);
|
||||
@ -205,7 +208,26 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA
|
||||
}));
|
||||
|
||||
try {
|
||||
const baseUrl = spec?.servers?.[0]?.url || '';
|
||||
let headersForRequest = requestState.headers;
|
||||
// Refresh admin access token before making request (if using admin auth)
|
||||
if (user && headersForRequest['X-Stack-Access-Type'] === 'admin') {
|
||||
const authJson = await user.getAuthJson();
|
||||
if (authJson.accessToken) {
|
||||
// Update the admin access token with a fresh one
|
||||
const refreshedHeaders: Record<string, string> = {
|
||||
...headersForRequest,
|
||||
'X-Stack-Admin-Access-Token': authJson.accessToken,
|
||||
};
|
||||
setRequestState(prev => ({ ...prev, headers: refreshedHeaders }));
|
||||
updateSharedHeaders(refreshedHeaders);
|
||||
headersForRequest = refreshedHeaders;
|
||||
}
|
||||
}
|
||||
// Use local API URL in development, production URL from OpenAPI spec otherwise
|
||||
const defaultBaseUrl = spec?.servers?.[0]?.url || '';
|
||||
const localApiUrl = process.env.NEXT_PUBLIC_STACK_API_URL;
|
||||
const baseUrl = localApiUrl ? localApiUrl + '/api/v1' : defaultBaseUrl;
|
||||
|
||||
let url = baseUrl + path;
|
||||
|
||||
// Replace path parameters
|
||||
@ -231,7 +253,7 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA
|
||||
|
||||
// Filter out empty headers
|
||||
const filteredHeaders = Object.fromEntries(
|
||||
Object.entries(requestState.headers).filter(([key, value]) => key && value)
|
||||
Object.entries(headersForRequest).filter(([key, value]) => key && value)
|
||||
);
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
@ -247,6 +269,11 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA
|
||||
);
|
||||
if (Object.keys(bodyData).length > 0) {
|
||||
requestOptions.body = JSON.stringify(bodyData);
|
||||
// Add Content-Type header when sending JSON body
|
||||
requestOptions.headers = {
|
||||
...filteredHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,7 +313,7 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [spec, requestState.parameters, requestState.headers, requestState.bodyFields, reportError]); // Changed from requestState.body
|
||||
}, [spec, requestState.parameters, requestState.headers, requestState.bodyFields, reportError, user, updateSharedHeaders]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@ -387,7 +414,11 @@ function ModernAPIPlayground({
|
||||
};
|
||||
|
||||
const generateCurlCommand = useCallback(() => {
|
||||
const baseUrl = spec.servers?.[0]?.url || '';
|
||||
// Use local API URL in development, production URL otherwise
|
||||
const defaultBaseUrl = spec.servers?.[0]?.url || '';
|
||||
const baseUrl = process.env.NEXT_PUBLIC_STACK_API_URL
|
||||
? process.env.NEXT_PUBLIC_STACK_API_URL + '/api/v1'
|
||||
: defaultBaseUrl;
|
||||
let url = baseUrl + path;
|
||||
|
||||
// Replace path parameters
|
||||
@ -433,7 +464,11 @@ function ModernAPIPlayground({
|
||||
return curlCommand;
|
||||
}, [operation, path, method, spec, requestState]);
|
||||
const generateJavaScriptCode = useCallback(() => {
|
||||
const baseUrl = spec.servers?.[0]?.url || '';
|
||||
// Use local API URL in development, production URL otherwise
|
||||
const defaultBaseUrl = spec.servers?.[0]?.url || '';
|
||||
const baseUrl = process.env.NEXT_PUBLIC_STACK_API_URL
|
||||
? process.env.NEXT_PUBLIC_STACK_API_URL + '/api/v1'
|
||||
: defaultBaseUrl;
|
||||
let url = baseUrl + path;
|
||||
|
||||
// Replace path parameters
|
||||
@ -483,7 +518,11 @@ function ModernAPIPlayground({
|
||||
}, [operation, path, method, spec, requestState]);
|
||||
|
||||
const generatePythonCode = useCallback(() => {
|
||||
const baseUrl = spec.servers?.[0]?.url || '';
|
||||
// Use local API URL in development, production URL otherwise
|
||||
const defaultBaseUrl = spec.servers?.[0]?.url || '';
|
||||
const baseUrl = process.env.NEXT_PUBLIC_STACK_API_URL
|
||||
? process.env.NEXT_PUBLIC_STACK_API_URL + '/api/v1'
|
||||
: defaultBaseUrl;
|
||||
let url = baseUrl + path;
|
||||
|
||||
// Replace path parameters
|
||||
|
||||
Loading…
Reference in New Issue
Block a user