chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* API Authentication Utilities
|
||||
* Validates API keys for workflow execution endpoints
|
||||
* Supports both Clerk (UI) and API Key (external) authentication
|
||||
*/
|
||||
|
||||
import { NextRequest } from 'next/server';
|
||||
import { getConvexClient, api } from '@/lib/convex/client';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
export interface ApiAuthResult {
|
||||
authenticated: boolean;
|
||||
userId?: string;
|
||||
error?: string;
|
||||
authType?: 'clerk' | 'api-key';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates authentication - checks Clerk first (for UI), then API key (for external)
|
||||
* This allows both authenticated UI users and API key users to access endpoints
|
||||
*/
|
||||
export async function validateApiKey(request: NextRequest): Promise<ApiAuthResult> {
|
||||
try {
|
||||
// First, check for Clerk authentication (UI users)
|
||||
const { userId } = await auth();
|
||||
if (userId) {
|
||||
return {
|
||||
authenticated: true,
|
||||
userId,
|
||||
authType: 'clerk'
|
||||
};
|
||||
}
|
||||
|
||||
// If no Clerk auth, check for API key (external API users)
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
|
||||
if (!authHeader) {
|
||||
return {
|
||||
authenticated: false,
|
||||
error: 'Authentication required. Please sign in or provide an API key with format: "Bearer YOUR_API_KEY"'
|
||||
};
|
||||
}
|
||||
|
||||
// Parse Bearer token
|
||||
const parts = authHeader.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
return {
|
||||
authenticated: false,
|
||||
error: 'Invalid Authorization header format. Expected: "Bearer YOUR_API_KEY"'
|
||||
};
|
||||
}
|
||||
|
||||
const apiKey = parts[1];
|
||||
|
||||
if (!apiKey || apiKey.length < 10) {
|
||||
return {
|
||||
authenticated: false,
|
||||
error: 'Invalid API key format'
|
||||
};
|
||||
}
|
||||
|
||||
// Verify key with Convex
|
||||
const convex = getConvexClient();
|
||||
const result = await convex.mutation(api.apiKeys.verify, { key: apiKey });
|
||||
|
||||
if (!result.valid) {
|
||||
return {
|
||||
authenticated: false,
|
||||
error: result.error || 'Invalid API key'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
userId: result.userId,
|
||||
authType: 'api-key'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('API key validation error:', error);
|
||||
return {
|
||||
authenticated: false,
|
||||
error: 'API key validation failed'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized unauthorized response
|
||||
*/
|
||||
export function createUnauthorizedResponse(error: string) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Unauthorized',
|
||||
message: error,
|
||||
hint: 'Generate an API key from Settings and include it in the Authorization header'
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'WWW-Authenticate': 'Bearer realm="API Key Required"'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user