Skip to content

Commit ca818a6

Browse files
authored
feat(admin): added admin APIs for admin management (#2206)
1 parent 1b903f2 commit ca818a6

File tree

19 files changed

+1970
-0
lines changed

19 files changed

+1970
-0
lines changed

apps/sim/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
2424
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
2525
# VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible)
2626
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth
27+
28+
# Admin API (Optional - for self-hosted GitOps)
29+
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.
30+
# Usage: curl -H "x-admin-key: your_key" https://your-instance/api/v1/admin/workspaces

apps/sim/app/api/v1/admin/auth.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Admin API Authentication
3+
*
4+
* Authenticates admin API requests using the ADMIN_API_KEY environment variable.
5+
* Designed for self-hosted deployments where GitOps/scripted access is needed.
6+
*
7+
* Usage:
8+
* curl -H "x-admin-key: your_admin_key" https://your-instance/api/v1/admin/...
9+
*/
10+
11+
import { createHash, timingSafeEqual } from 'crypto'
12+
import type { NextRequest } from 'next/server'
13+
import { env } from '@/lib/core/config/env'
14+
import { createLogger } from '@/lib/logs/console/logger'
15+
16+
const logger = createLogger('AdminAuth')
17+
18+
export interface AdminAuthSuccess {
19+
authenticated: true
20+
}
21+
22+
export interface AdminAuthFailure {
23+
authenticated: false
24+
error: string
25+
notConfigured?: boolean
26+
}
27+
28+
export type AdminAuthResult = AdminAuthSuccess | AdminAuthFailure
29+
30+
/**
31+
* Authenticate an admin API request.
32+
*
33+
* @param request - The incoming Next.js request
34+
* @returns Authentication result with success status and optional error
35+
*/
36+
export function authenticateAdminRequest(request: NextRequest): AdminAuthResult {
37+
const adminKey = env.ADMIN_API_KEY
38+
39+
if (!adminKey) {
40+
logger.warn('ADMIN_API_KEY environment variable is not set')
41+
return {
42+
authenticated: false,
43+
error: 'Admin API is not configured. Set ADMIN_API_KEY environment variable.',
44+
notConfigured: true,
45+
}
46+
}
47+
48+
const providedKey = request.headers.get('x-admin-key')
49+
50+
if (!providedKey) {
51+
return {
52+
authenticated: false,
53+
error: 'Admin API key required. Provide x-admin-key header.',
54+
}
55+
}
56+
57+
if (!constantTimeCompare(providedKey, adminKey)) {
58+
logger.warn('Invalid admin API key attempted', { keyPrefix: providedKey.slice(0, 8) })
59+
return {
60+
authenticated: false,
61+
error: 'Invalid admin API key',
62+
}
63+
}
64+
65+
return { authenticated: true }
66+
}
67+
68+
/**
69+
* Constant-time string comparison.
70+
*
71+
* @param a - First string to compare
72+
* @param b - Second string to compare
73+
* @returns True if strings are equal, false otherwise
74+
*/
75+
function constantTimeCompare(a: string, b: string): boolean {
76+
const aHash = createHash('sha256').update(a).digest()
77+
const bHash = createHash('sha256').update(b).digest()
78+
return timingSafeEqual(aHash, bHash)
79+
}

apps/sim/app/api/v1/admin/index.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Admin API v1
3+
*
4+
* A RESTful API for administrative operations on Sim.
5+
*
6+
* Authentication:
7+
* Set ADMIN_API_KEY environment variable and use x-admin-key header.
8+
*
9+
* Endpoints:
10+
* GET /api/v1/admin/users - List all users
11+
* GET /api/v1/admin/users/:id - Get user details
12+
* GET /api/v1/admin/workspaces - List all workspaces
13+
* GET /api/v1/admin/workspaces/:id - Get workspace details
14+
* GET /api/v1/admin/workspaces/:id/workflows - List workspace workflows
15+
* DELETE /api/v1/admin/workspaces/:id/workflows - Delete all workspace workflows
16+
* GET /api/v1/admin/workspaces/:id/folders - List workspace folders
17+
* GET /api/v1/admin/workspaces/:id/export - Export workspace (ZIP/JSON)
18+
* POST /api/v1/admin/workspaces/:id/import - Import into workspace
19+
* GET /api/v1/admin/workflows - List all workflows
20+
* GET /api/v1/admin/workflows/:id - Get workflow details
21+
* DELETE /api/v1/admin/workflows/:id - Delete workflow
22+
* GET /api/v1/admin/workflows/:id/export - Export workflow (JSON)
23+
* POST /api/v1/admin/workflows/import - Import single workflow
24+
*/
25+
26+
export type { AdminAuthFailure, AdminAuthResult, AdminAuthSuccess } from '@/app/api/v1/admin/auth'
27+
export { authenticateAdminRequest } from '@/app/api/v1/admin/auth'
28+
export type { AdminRouteHandler, AdminRouteHandlerWithParams } from '@/app/api/v1/admin/middleware'
29+
export { withAdminAuth, withAdminAuthParams } from '@/app/api/v1/admin/middleware'
30+
export {
31+
badRequestResponse,
32+
errorResponse,
33+
forbiddenResponse,
34+
internalErrorResponse,
35+
listResponse,
36+
notConfiguredResponse,
37+
notFoundResponse,
38+
singleResponse,
39+
unauthorizedResponse,
40+
} from '@/app/api/v1/admin/responses'
41+
export type {
42+
AdminErrorResponse,
43+
AdminFolder,
44+
AdminListResponse,
45+
AdminSingleResponse,
46+
AdminUser,
47+
AdminWorkflow,
48+
AdminWorkflowDetail,
49+
AdminWorkspace,
50+
AdminWorkspaceDetail,
51+
DbUser,
52+
DbWorkflow,
53+
DbWorkflowFolder,
54+
DbWorkspace,
55+
FolderExportPayload,
56+
ImportResult,
57+
PaginationMeta,
58+
PaginationParams,
59+
VariableType,
60+
WorkflowExportPayload,
61+
WorkflowExportState,
62+
WorkflowImportRequest,
63+
WorkflowVariable,
64+
WorkspaceExportPayload,
65+
WorkspaceImportRequest,
66+
WorkspaceImportResponse,
67+
} from '@/app/api/v1/admin/types'
68+
export {
69+
createPaginationMeta,
70+
DEFAULT_LIMIT,
71+
extractWorkflowMetadata,
72+
MAX_LIMIT,
73+
parsePaginationParams,
74+
parseWorkflowVariables,
75+
toAdminFolder,
76+
toAdminUser,
77+
toAdminWorkflow,
78+
toAdminWorkspace,
79+
} from '@/app/api/v1/admin/types'
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { NextRequest, NextResponse } from 'next/server'
2+
import { authenticateAdminRequest } from '@/app/api/v1/admin/auth'
3+
import { notConfiguredResponse, unauthorizedResponse } from '@/app/api/v1/admin/responses'
4+
5+
export type AdminRouteHandler = (request: NextRequest) => Promise<NextResponse>
6+
7+
export type AdminRouteHandlerWithParams<TParams> = (
8+
request: NextRequest,
9+
context: { params: Promise<TParams> }
10+
) => Promise<NextResponse>
11+
12+
/**
13+
* Wrap a route handler with admin authentication.
14+
* Returns early with an error response if authentication fails.
15+
*/
16+
export function withAdminAuth(handler: AdminRouteHandler): AdminRouteHandler {
17+
return async (request: NextRequest) => {
18+
const auth = authenticateAdminRequest(request)
19+
20+
if (!auth.authenticated) {
21+
if (auth.notConfigured) {
22+
return notConfiguredResponse()
23+
}
24+
return unauthorizedResponse(auth.error)
25+
}
26+
27+
return handler(request)
28+
}
29+
}
30+
31+
/**
32+
* Wrap a route handler with params with admin authentication.
33+
* Returns early with an error response if authentication fails.
34+
*/
35+
export function withAdminAuthParams<TParams>(
36+
handler: AdminRouteHandlerWithParams<TParams>
37+
): AdminRouteHandlerWithParams<TParams> {
38+
return async (request: NextRequest, context: { params: Promise<TParams> }) => {
39+
const auth = authenticateAdminRequest(request)
40+
41+
if (!auth.authenticated) {
42+
if (auth.notConfigured) {
43+
return notConfiguredResponse()
44+
}
45+
return unauthorizedResponse(auth.error)
46+
}
47+
48+
return handler(request, context)
49+
}
50+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Admin API Response Helpers
3+
*
4+
* Consistent response formatting for all Admin API endpoints.
5+
*/
6+
7+
import { NextResponse } from 'next/server'
8+
import type {
9+
AdminErrorResponse,
10+
AdminListResponse,
11+
AdminSingleResponse,
12+
PaginationMeta,
13+
} from '@/app/api/v1/admin/types'
14+
15+
/**
16+
* Create a successful list response with pagination
17+
*/
18+
export function listResponse<T>(
19+
data: T[],
20+
pagination: PaginationMeta
21+
): NextResponse<AdminListResponse<T>> {
22+
return NextResponse.json({ data, pagination })
23+
}
24+
25+
/**
26+
* Create a successful single resource response
27+
*/
28+
export function singleResponse<T>(data: T): NextResponse<AdminSingleResponse<T>> {
29+
return NextResponse.json({ data })
30+
}
31+
32+
/**
33+
* Create an error response
34+
*/
35+
export function errorResponse(
36+
code: string,
37+
message: string,
38+
status: number,
39+
details?: unknown
40+
): NextResponse<AdminErrorResponse> {
41+
const body: AdminErrorResponse = {
42+
error: { code, message },
43+
}
44+
45+
if (details !== undefined) {
46+
body.error.details = details
47+
}
48+
49+
return NextResponse.json(body, { status })
50+
}
51+
52+
// =============================================================================
53+
// Common Error Responses
54+
// =============================================================================
55+
56+
export function unauthorizedResponse(message = 'Authentication required'): NextResponse {
57+
return errorResponse('UNAUTHORIZED', message, 401)
58+
}
59+
60+
export function forbiddenResponse(message = 'Access denied'): NextResponse {
61+
return errorResponse('FORBIDDEN', message, 403)
62+
}
63+
64+
export function notFoundResponse(resource: string): NextResponse {
65+
return errorResponse('NOT_FOUND', `${resource} not found`, 404)
66+
}
67+
68+
export function badRequestResponse(message: string, details?: unknown): NextResponse {
69+
return errorResponse('BAD_REQUEST', message, 400, details)
70+
}
71+
72+
export function internalErrorResponse(message = 'Internal server error'): NextResponse {
73+
return errorResponse('INTERNAL_ERROR', message, 500)
74+
}
75+
76+
export function notConfiguredResponse(): NextResponse {
77+
return errorResponse(
78+
'NOT_CONFIGURED',
79+
'Admin API is not configured. Set ADMIN_API_KEY environment variable.',
80+
503
81+
)
82+
}

0 commit comments

Comments
 (0)