diff --git a/.changeset/green-banks-juggle.md b/.changeset/green-banks-juggle.md new file mode 100644 index 0000000000..26569a2f78 --- /dev/null +++ b/.changeset/green-banks-juggle.md @@ -0,0 +1,4 @@ +--- +--- + +Add static admin API key for programmatic access (ADMIN_API_KEY env var) diff --git a/.env.local.example b/.env.local.example index 3b8acd5a64..94909eb8ca 100644 --- a/.env.local.example +++ b/.env.local.example @@ -68,6 +68,11 @@ WORKOS_REDIRECT_URI=http://localhost:3000/auth/callback # Users with these emails will have access to /admin/* endpoints # ADMIN_EMAILS=admin@example.com,owner@example.com +# Admin API Key (for programmatic admin access without browser session) +# Generate with: node -e "console.log('aao_' + require('crypto').randomBytes(32).toString('base64url'))" +# Use as: Authorization: Bearer +# ADMIN_API_KEY=aao_your_secret_key_here + # Allow insecure cookies for local development (HTTP instead of HTTPS) # Set to true when running locally with Docker over HTTP # ALLOW_INSECURE_COOKIES=true diff --git a/server/public/admin-api-keys.html b/server/public/admin-api-keys.html index 4efdc564f1..aeb9dc0f5e 100644 --- a/server/public/admin-api-keys.html +++ b/server/public/admin-api-keys.html @@ -31,45 +31,6 @@ font-size: var(--text-sm); margin-bottom: var(--space-6); } - .org-selector { - margin-bottom: var(--space-6); - } - .org-selector label { - display: block; - margin-bottom: var(--space-2); - font-weight: 500; - color: var(--color-text-heading); - } - .org-selector select { - width: 100%; - max-width: 400px; - padding: var(--space-2.5) var(--space-3); - border: var(--border-1) solid var(--color-gray-200); - border-radius: var(--radius-md); - font-size: var(--text-sm); - background: var(--color-bg-card); - } - .widget-container { - min-height: 400px; - border: var(--border-1) solid var(--color-gray-200); - border-radius: var(--radius-md); - padding: var(--space-4); - } - .loading { - display: flex; - align-items: center; - justify-content: center; - height: 200px; - color: var(--color-text-secondary); - } - .error-message { - background: var(--color-error-50); - border: var(--border-1) solid var(--color-error-200); - color: var(--color-error-700); - padding: var(--space-4); - border-radius: var(--radius-md); - margin-bottom: var(--space-4); - } .info-box { background: var(--color-primary-50); border: var(--border-1) solid var(--color-primary-200); @@ -85,6 +46,35 @@ border-radius: var(--radius-sm); font-family: var(--font-mono); } + .code-block { + background: var(--color-gray-900); + color: var(--color-gray-100); + padding: var(--space-4); + border-radius: var(--radius-md); + font-family: var(--font-mono); + font-size: var(--text-sm); + overflow-x: auto; + margin: var(--space-4) 0; + } + .code-block .comment { + color: var(--color-gray-500); + } + .code-block .string { + color: var(--color-success-400); + } + .section { + margin-bottom: var(--space-6); + } + .section h2 { + font-size: var(--text-lg); + color: var(--color-text-heading); + margin-bottom: var(--space-3); + } + .section p { + color: var(--color-text-secondary); + font-size: var(--text-sm); + line-height: 1.6; + } .permissions-list { margin-top: var(--space-4); } @@ -123,181 +113,63 @@

API Keys

-

Manage API keys for programmatic access to the admin API

+

Programmatic access to the admin API

How API Keys Work:

- API keys are organization-scoped and can be used to access the admin API without a browser session. + API keys provide programmatic access to admin endpoints without requiring a browser session. Use them in the Authorization: Bearer <key> header.

-
-

Available Permissions

-
    -
  • admin:* - Full admin access (read and write)
  • -
  • admin:read - Read-only admin access
  • -
-
- +
+

Admin API Key

+

+ Set the ADMIN_API_KEY environment variable to enable programmatic admin access. + This key grants full admin permissions to all API endpoints. +

-
- - +
+ # Generate a secure key
+ node -e "console.log('aao_' + require('crypto').randomBytes(32).toString('base64url'))"

+ # Add to .env.local
+ ADMIN_API_KEY=aao_your_generated_key

+ # Use in API requests
+ curl -H "Authorization: Bearer $ADMIN_API_KEY" \
+   https://agenticadvertising.org/api/admin/users +
- +
+

Usage Examples

+ +
+ # List all users
+ curl -H "Authorization: Bearer $ADMIN_API_KEY" \
+   /api/admin/users

+ # Get organization details
+ curl -H "Authorization: Bearer $ADMIN_API_KEY" \
+   /api/admin/organizations

+ # View analytics
+ curl -H "Authorization: Bearer $ADMIN_API_KEY" \
+   /api/admin/analytics/dashboard +
+
-
-
Select an organization to manage API keys
+
+

API Endpoints Available

+
    +
  • GET /api/admin/users - List all users
  • +
  • GET /api/admin/organizations - List organizations
  • +
  • GET /api/admin/analytics/* - Analytics data
  • +
  • GET /api/admin/working-groups - Working groups
  • +
  • POST /api/admin/* - All write operations
  • +
- - diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 1d3e5739e5..d235f914b4 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -255,17 +255,56 @@ function setSessionCookie(res: Response, sealedSession: string) { }); } +// Static admin API key for internal tooling (bypasses WorkOS) +const ADMIN_API_KEY = process.env.ADMIN_API_KEY; +if (ADMIN_API_KEY) { + logger.info('Admin API key configured for programmatic access'); +} + +/** + * Check if request has a valid static admin API key + * Returns true if the Bearer token matches ADMIN_API_KEY + */ +function hasValidAdminApiKey(req: Request): boolean { + if (!ADMIN_API_KEY) return false; + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith('Bearer ')) return false; + const token = authHeader.slice(7); + // Don't match WorkOS API keys - those are handled separately + if (token.startsWith('wos_api_key_')) return false; + return token === ADMIN_API_KEY; +} + /** * Middleware to require authentication * Checks for WorkOS session cookie and loads user info * Also accepts WorkOS API keys as Bearer token for programmatic access + * Also accepts static admin API key (ADMIN_API_KEY env var) for internal tooling * Uses in-memory cache to reduce WorkOS API calls for session refresh * Automatically refreshes expired access tokens using the refresh token */ export async function requireAuth(req: Request, res: Response, next: NextFunction) { const isHtmlRequest = req.accepts('html') && !req.path.startsWith('/api/'); - // Check for WorkOS API key first (for programmatic access) + // Check for static admin API key first (for internal tooling) + if (hasValidAdminApiKey(req)) { + logger.debug({ path: req.path }, 'Authenticated via static admin API key'); + req.user = { + id: 'admin_api_key', + email: 'admin-api-key@internal', + firstName: 'Admin', + lastName: 'API Key', + emailVerified: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + req.accessToken = 'admin-api-key'; + // Mark this request as using the static admin API key for requireAdmin check + (req as Request & { isStaticAdminApiKey?: boolean }).isStaticAdminApiKey = true; + return next(); + } + + // Check for WorkOS API key (for programmatic access) const apiKey = await validateWorkOSApiKey(req); if (apiKey) { logger.debug({ path: req.path, apiKeyId: apiKey.id }, 'Authenticated via WorkOS API key'); @@ -587,12 +626,19 @@ export function requireRole(...allowedRoles: Array<'owner' | 'admin' | 'member'> /** * Middleware to require admin access * Must be used after requireAuth + * Accepts static admin API key (ADMIN_API_KEY env var) for internal tooling * Accepts WorkOS API keys with 'admin:*' permission for programmatic access * Or checks if user's email is in ADMIN_EMAILS list */ export async function requireAdmin(req: Request, res: Response, next: NextFunction) { const isHtmlRequest = req.accepts('html') && !req.path.startsWith('/api/'); + // Check for static admin API key (set by requireAuth) + if ((req as Request & { isStaticAdminApiKey?: boolean }).isStaticAdminApiKey) { + logger.debug({ path: req.path, method: req.method }, 'Admin access via static admin API key'); + return next(); + } + // Check for WorkOS API key with admin permission const apiKey = (req as Request & { apiKey?: ValidatedApiKey }).apiKey; if (apiKey) {