From 3c73e2c5ea26d39245b171da1405b17ba207ff5c Mon Sep 17 00:00:00 2001 From: Vigneshraj Sekar Babu Date: Fri, 5 Sep 2025 10:16:33 -0700 Subject: [PATCH] remove api key auth removing in favor of external auth management --- src/pages/api/v1/admin/api-keys/[id].ts | 335 ---------------- src/pages/api/v1/admin/api-keys/index.ts | 371 ------------------ src/pages/api/v1/builds/[uuid]/deploy.ts | 6 - src/pages/api/v1/builds/[uuid]/graph.ts | 6 - src/pages/api/v1/builds/[uuid]/index.ts | 6 - .../v1/builds/[uuid]/jobs/[jobName]/events.ts | 6 - .../v1/builds/[uuid]/jobs/[jobName]/logs.ts | 6 - .../v1/builds/[uuid]/services/[name]/build.ts | 6 - .../[uuid]/services/[name]/buildLogs.ts | 6 - .../services/[name]/buildLogs/[jobName].ts | 6 - .../[uuid]/services/[name]/deployLogs.ts | 6 - .../services/[name]/deployLogs/[jobName].ts | 6 - .../[uuid]/services/[name]/deployment.ts | 6 - .../v1/builds/[uuid]/services/[name]/logs.ts | 6 - .../[uuid]/services/[name]/logs/[jobName].ts | 6 - src/pages/api/v1/builds/[uuid]/torndown.ts | 6 - src/pages/api/v1/builds/[uuid]/webhooks.ts | 8 - src/pages/api/v1/builds/index.ts | 6 - src/pages/api/v1/config/cache.ts | 8 - src/pages/api/v1/deploy-summary.ts | 6 - src/pages/api/v1/deployables.ts | 6 - src/pages/api/v1/deploys.ts | 6 - src/pages/api/v1/pull-requests/[id]/builds.ts | 6 - src/pages/api/v1/pull-requests/[id]/index.ts | 6 - src/pages/api/v1/pull-requests/index.ts | 6 - src/pages/api/v1/repos/index.ts | 6 - src/pages/api/v1/schema/validate.ts | 6 - src/pages/api/v1/users/index.ts | 6 - src/pages/v1/docs.tsx | 10 - src/server/db/migrations/003_add_api_keys.ts | 31 -- src/server/lib/auth/constants.ts | 22 -- src/server/lib/auth/keyGenerator.ts | 82 ---- src/server/lib/auth/rateLimiter.ts | 60 --- src/server/lib/auth/validate.ts | 84 ---- src/server/models/ApiKey.ts | 109 ----- src/server/services/auth.ts | 176 --------- src/server/services/types/globalConfig.ts | 8 - swaggerSpec.ts | 10 - 38 files changed, 1458 deletions(-) delete mode 100644 src/pages/api/v1/admin/api-keys/[id].ts delete mode 100644 src/pages/api/v1/admin/api-keys/index.ts delete mode 100644 src/server/db/migrations/003_add_api_keys.ts delete mode 100644 src/server/lib/auth/constants.ts delete mode 100644 src/server/lib/auth/keyGenerator.ts delete mode 100644 src/server/lib/auth/rateLimiter.ts delete mode 100644 src/server/lib/auth/validate.ts delete mode 100644 src/server/models/ApiKey.ts delete mode 100644 src/server/services/auth.ts diff --git a/src/pages/api/v1/admin/api-keys/[id].ts b/src/pages/api/v1/admin/api-keys/[id].ts deleted file mode 100644 index ff7fde29..00000000 --- a/src/pages/api/v1/admin/api-keys/[id].ts +++ /dev/null @@ -1,335 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { NextApiRequest, NextApiResponse } from 'next'; -import AuthService from 'server/services/auth'; -import { validateAuth } from 'server/lib/auth/validate'; -import rootLogger from 'server/lib/logger'; - -const logger = rootLogger.child({ - filename: 'api/v1/admin/api-keys/[id].ts', -}); - -/** - * @openapi - * /api/v1/admin/api-keys/{id}: - * put: - * summary: Update an API key - * description: | - * Updates metadata for an existing API key. The actual key value cannot be changed. - * Requires API key authentication. - * tags: - * - Admin - * security: - * - ApiKeyAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: The API key ID - * example: 1 - * requestBody: - * required: false - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * description: Human-readable name for the API key - * example: "Updated Production API Key" - * description: - * type: string - * description: Optional description of the API key's purpose - * example: "Updated description for production service integrations" - * scopes: - * type: array - * items: - * type: string - * description: Permission scopes for the API key (future use) - * example: ["read", "write", "admin"] - * responses: - * 200: - * description: API key updated successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "API key updated successfully" - * apiKey: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * keyId: - * type: string - * example: "Ab1Cd2Ef" - * name: - * type: string - * example: "Updated Production API Key" - * description: - * type: string - * example: "Updated description for production service integrations" - * scopes: - * type: array - * items: - * type: string - * example: ["read", "write", "admin"] - * active: - * type: boolean - * example: true - * githubUserId: - * type: integer - * example: 12345 - * githubLogin: - * type: string - * example: "johndoe" - * updatedAt: - * type: string - * format: date-time - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Bad Request" - * 401: - * description: Unauthorized - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Unauthorized" - * 404: - * description: API key not found - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Not Found" - * 429: - * description: Too many requests - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Too Many Requests" - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Internal Server Error" - * delete: - * summary: Revoke an API key - * description: | - * Revokes (deactivates) an existing API key. The key will no longer be able to - * authenticate requests. This action cannot be undone. Requires API key authentication. - * tags: - * - Admin - * security: - * - ApiKeyAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: The API key ID - * example: 1 - * responses: - * 200: - * description: API key revoked successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "API key revoked successfully" - * id: - * type: integer - * example: 1 - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Bad Request" - * 401: - * description: Unauthorized - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Unauthorized" - * 404: - * description: API key not found - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Not Found" - * 429: - * description: Too many requests - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Too Many Requests" - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Internal Server Error" - */ -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { valid } = await validateAuth(req, res); - if (!valid) return; - - try { - const authService = new AuthService(); - const { id } = req.query; - - const apiKeyId = parseInt(id as string, 10); - if (isNaN(apiKeyId)) { - logger.error('Invalid API key ID provided', { id }); - return res.status(400).json({ error: 'Bad Request' }); - } - - switch (req.method) { - case 'PUT': - return await handleUpdateApiKey(req, res, authService, apiKeyId); - - case 'DELETE': - return await handleRevokeApiKey(req, res, authService, apiKeyId); - - default: - res.setHeader('Allow', ['PUT', 'DELETE']); - return res.status(405).json({ error: 'Method Not Allowed' }); - } - } catch (error) { - logger.error('Admin API key endpoint error:', error); - return res.status(500).json({ error: 'Internal Server Error' }); - } -} - -async function handleUpdateApiKey( - req: NextApiRequest, - res: NextApiResponse, - authService: AuthService, - apiKeyId: number -) { - const { name, description, scopes } = req.body; - - try { - const updatedKey = await authService.updateApiKey(apiKeyId, { - name, - description, - scopes, - }); - - if (!updatedKey) { - logger.error('API key not found for update', { apiKeyId }); - return res.status(404).json({ error: 'Not Found' }); - } - - return res.status(200).json({ - message: 'API key updated successfully', - apiKey: { - id: updatedKey.id, - keyId: updatedKey.keyId, - name: updatedKey.name, - description: updatedKey.description, - scopes: updatedKey.scopes, - active: updatedKey.active, - githubUserId: updatedKey.githubUserId, - githubLogin: updatedKey.githubLogin, - updatedAt: updatedKey.updatedAt, - }, - }); - } catch (error) { - logger.error('Failed to update API key:', error); - return res.status(500).json({ error: 'Internal Server Error' }); - } -} - -async function handleRevokeApiKey( - req: NextApiRequest, - res: NextApiResponse, - authService: AuthService, - apiKeyId: number -) { - try { - const revoked = await authService.revokeApiKey(apiKeyId); - - if (!revoked) { - logger.error('API key not found for revocation', { apiKeyId }); - return res.status(404).json({ error: 'Not Found' }); - } - - return res.status(200).json({ - message: 'API key revoked successfully', - id: apiKeyId, - }); - } catch (error) { - logger.error('Failed to revoke API key:', error); - return res.status(500).json({ error: 'Internal Server Error' }); - } -} diff --git a/src/pages/api/v1/admin/api-keys/index.ts b/src/pages/api/v1/admin/api-keys/index.ts deleted file mode 100644 index 46f33794..00000000 --- a/src/pages/api/v1/admin/api-keys/index.ts +++ /dev/null @@ -1,371 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { NextApiRequest, NextApiResponse } from 'next'; -import AuthService from 'server/services/auth'; -import { validateAuth } from 'server/lib/auth/validate'; -import rootLogger from 'server/lib/logger'; - -const logger = rootLogger.child({ - filename: 'api/v1/admin/api-keys/index.ts', -}); - -/** - * @openapi - * /api/v1/admin/api-keys: - * post: - * summary: Create a new API key - * description: | - * Creates a new API key for authentication. The full key is only returned once - * and cannot be retrieved again. - * - * **Bootstrap Mode**: If no API keys exist in the database, this endpoint - * allows creating the first key using a bootstrap token (X-Bootstrap-Token header). - * The bootstrap token is provided via the APP_BOOTSTRAP_TOKEN environment variable - * during deployment. After the first key is created, all subsequent requests - * require API key authentication. - * tags: - * - Admin - * security: - * - ApiKeyAuth: [] - * parameters: - * - in: header - * name: X-Bootstrap-Token - * required: false - * description: | - * Bootstrap token required only when creating the first API key. - * Must match the APP_BOOTSTRAP_TOKEN environment variable. - * schema: - * type: string - * example: "abc123def456ghi789" - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * properties: - * name: - * type: string - * description: Human-readable name for the API key - * example: "Production API Key" - * description: - * type: string - * description: Optional description of the API key's purpose - * example: "Used for production service integrations" - * githubUserId: - * type: integer - * description: GitHub user ID associated with this key - * example: 12345 - * githubLogin: - * type: string - * description: GitHub username associated with this key - * example: "johndoe" - * scopes: - * type: array - * items: - * type: string - * description: Permission scopes for the API key (future use) - * example: ["read", "write"] - * responses: - * 201: - * description: API key created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "API key created successfully" - * apiKey: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * keyId: - * type: string - * example: "Ab1Cd2Ef" - * name: - * type: string - * example: "Production API Key" - * description: - * type: string - * example: "Used for production service integrations" - * scopes: - * type: array - * items: - * type: string - * example: ["read", "write"] - * githubUserId: - * type: integer - * example: 12345 - * githubLogin: - * type: string - * example: "johndoe" - * createdAt: - * type: string - * format: date-time - * fullKey: - * type: string - * description: The complete API key (shown only once) - * example: "lfc_Ab1Cd2Ef_9xKzPqR8sT2vN7wE3mF6aL4bC8nQ1uY5" - * warning: - * type: string - * example: "Please save this key securely. It cannot be retrieved again." - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Bad Request" - * 401: - * description: Unauthorized - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Unauthorized" - * 429: - * description: Too many requests - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Too Many Requests" - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Internal Server Error" - * get: - * summary: List all API keys - * description: | - * Retrieves a list of all API keys with sensitive information masked. - * Only shows metadata, not the actual key values. Requires API key authentication. - * tags: - * - Admin - * security: - * - ApiKeyAuth: [] - * responses: - * 200: - * description: Successfully retrieved API keys list - * content: - * application/json: - * schema: - * type: object - * properties: - * apiKeys: - * type: array - * items: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * keyId: - * type: string - * description: Public portion of the API key - * example: "Ab1Cd2Ef" - * name: - * type: string - * example: "Production API Key" - * description: - * type: string - * example: "Used for production service integrations" - * active: - * type: boolean - * example: true - * githubUserId: - * type: integer - * example: 12345 - * githubLogin: - * type: string - * example: "johndoe" - * createdAt: - * type: string - * format: date-time - * updatedAt: - * type: string - * format: date-time - * lastUsedAt: - * type: string - * format: date-time - * nullable: true - * total: - * type: integer - * description: Total number of API keys - * example: 5 - * 401: - * description: Unauthorized - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Unauthorized" - * 429: - * description: Too many requests - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Too Many Requests" - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Internal Server Error" - */ -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const authService = new AuthService(); - - // Bootstrap mode: Allow first API key creation with bootstrap token - if (req.method === 'POST') { - const config = await authService.getApiConfig(); - - // Skip bootstrap check if auth is disabled globally - if (config.requireAuth) { - const hasKeys = await authService.hasApiKeys(); - - if (!hasKeys) { - const providedToken = req.headers['x-bootstrap-token'] as string; - const bootstrapToken = process.env.APP_BOOTSTRAP_TOKEN; - - if (bootstrapToken && providedToken === bootstrapToken) { - logger.info('Bootstrap mode: Creating first API key with bootstrap token'); - return await handleCreateApiKey(req, res, authService); - } else { - logger.warn('Bootstrap mode: Invalid or missing bootstrap token'); - return res.status(401).json({ error: 'Unauthorized' }); - } - } - } - } - - // Validate auth if api keys exist already - const { valid } = await validateAuth(req, res); - if (!valid) return; - - switch (req.method) { - case 'POST': - return await handleCreateApiKey(req, res, authService); - - case 'GET': - return await handleListApiKeys(req, res, authService); - - default: - res.setHeader('Allow', ['GET', 'POST']); - return res.status(405).json({ error: 'Method Not Allowed' }); - } - } catch (error) { - logger.error('Admin API key endpoint error:', error); - return res.status(500).json({ error: 'Internal Server Error' }); - } -} - -async function handleCreateApiKey(req: NextApiRequest, res: NextApiResponse, authService: AuthService) { - const { name, description, githubUserId, githubLogin, scopes } = req.body; - - if (!name) { - logger.error('API key creation attempted without name'); - return res.status(400).json({ error: 'Bad Request' }); - } - - try { - const result = await authService.createApiKey({ - name, - description, - githubUserId, - githubLogin, - scopes, - }); - - // Return the full key only on creation - return res.status(201).json({ - message: 'API key created successfully', - apiKey: { - id: result.apiKey.id, - keyId: result.apiKey.keyId, - name: result.apiKey.name, - description: result.apiKey.description, - scopes: result.apiKey.scopes, - githubUserId: result.apiKey.githubUserId, - githubLogin: result.apiKey.githubLogin, - createdAt: result.apiKey.createdAt, - }, - fullKey: result.fullKey, - warning: 'Please save this key securely. It cannot be retrieved again.', - }); - } catch (error) { - logger.error('Failed to create API key:', error); - return res.status(500).json({ error: 'Internal Server Error' }); - } -} - -async function handleListApiKeys(req: NextApiRequest, res: NextApiResponse, authService: AuthService) { - try { - const apiKeys = await authService.listApiKeys(); - - return res.status(200).json({ - apiKeys: apiKeys.map((key) => ({ - id: key.id, - keyId: key.keyId, - name: key.name, - description: key.description, - active: key.active, - githubUserId: key.githubUserId, - githubLogin: key.githubLogin, - createdAt: key.createdAt, - updatedAt: key.updatedAt, - lastUsedAt: key.lastUsedAt, - })), - total: apiKeys.length, - }); - } catch (error) { - logger.error('Failed to list API keys:', error); - return res.status(500).json({ error: 'Internal Server Error' }); - } -} diff --git a/src/pages/api/v1/builds/[uuid]/deploy.ts b/src/pages/api/v1/builds/[uuid]/deploy.ts index 353f2f2a..4f170aa2 100644 --- a/src/pages/api/v1/builds/[uuid]/deploy.ts +++ b/src/pages/api/v1/builds/[uuid]/deploy.ts @@ -19,7 +19,6 @@ import rootLogger from 'server/lib/logger'; import { Build } from 'server/models'; import { nanoid } from 'nanoid'; import BuildService from 'server/services/build'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'builds/[uuid]/deploy.ts', @@ -35,8 +34,6 @@ const logger = rootLogger.child({ * will be queued for deployment and its status will be updated accordingly. * tags: * - Builds - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -95,9 +92,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid } = req.query; try { diff --git a/src/pages/api/v1/builds/[uuid]/graph.ts b/src/pages/api/v1/builds/[uuid]/graph.ts index bdd70a8b..25a31da4 100644 --- a/src/pages/api/v1/builds/[uuid]/graph.ts +++ b/src/pages/api/v1/builds/[uuid]/graph.ts @@ -19,7 +19,6 @@ import { generateGraph } from 'server/lib/dependencyGraph'; import rootLogger from 'server/lib/logger'; import { Build } from 'server/models'; import BuildService from 'server/services/build'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'builds/[uuid]/graph.ts', @@ -36,8 +35,6 @@ const logger = rootLogger.child({ * relationships between different deployables in the build. * tags: * - Builds - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -89,9 +86,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid } = req.query; try { diff --git a/src/pages/api/v1/builds/[uuid]/index.ts b/src/pages/api/v1/builds/[uuid]/index.ts index 8d2566d0..5d56c667 100644 --- a/src/pages/api/v1/builds/[uuid]/index.ts +++ b/src/pages/api/v1/builds/[uuid]/index.ts @@ -17,7 +17,6 @@ import { NextApiRequest, NextApiResponse } from 'next/types'; import rootLogger from 'server/lib/logger'; import BuildService from 'server/services/build'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'builds/[uuid]/index.ts', @@ -32,8 +31,6 @@ const logger = rootLogger.child({ * Retrieves detailed information about a specific build by its UUID. * tags: * - Builds - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -109,9 +106,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid } = req.query; if (!uuid || typeof uuid !== 'string') { diff --git a/src/pages/api/v1/builds/[uuid]/jobs/[jobName]/events.ts b/src/pages/api/v1/builds/[uuid]/jobs/[jobName]/events.ts index 44cb9f20..6ea5e876 100644 --- a/src/pages/api/v1/builds/[uuid]/jobs/[jobName]/events.ts +++ b/src/pages/api/v1/builds/[uuid]/jobs/[jobName]/events.ts @@ -25,8 +25,6 @@ * tags: * - Jobs * - Events - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -147,7 +145,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import rootLogger from 'server/lib/logger'; import * as k8s from '@kubernetes/client-node'; import { HttpError } from '@kubernetes/client-node'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: __filename, @@ -235,9 +232,6 @@ const eventsHandler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid, jobName } = req.query; if (typeof uuid !== 'string' || typeof jobName !== 'string') { diff --git a/src/pages/api/v1/builds/[uuid]/jobs/[jobName]/logs.ts b/src/pages/api/v1/builds/[uuid]/jobs/[jobName]/logs.ts index 3c6283ba..ebb62b1f 100644 --- a/src/pages/api/v1/builds/[uuid]/jobs/[jobName]/logs.ts +++ b/src/pages/api/v1/builds/[uuid]/jobs/[jobName]/logs.ts @@ -17,7 +17,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import rootLogger from 'server/lib/logger'; import unifiedLogStreamHandler from '../../services/[name]/logs/[jobName]'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: __filename, @@ -35,8 +34,6 @@ const logger = rootLogger.child({ * tags: * - Webhooks * - Jobs - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -104,9 +101,6 @@ const logger = rootLogger.child({ * description: Internal server error */ export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { valid } = await validateAuth(req, res); - if (!valid) return; - logger.info( `method=${req.method} jobName=${req.query.jobName} message="Job logs endpoint called, delegating to unified handler"` ); diff --git a/src/pages/api/v1/builds/[uuid]/services/[name]/build.ts b/src/pages/api/v1/builds/[uuid]/services/[name]/build.ts index 538ae5dc..450199da 100644 --- a/src/pages/api/v1/builds/[uuid]/services/[name]/build.ts +++ b/src/pages/api/v1/builds/[uuid]/services/[name]/build.ts @@ -22,7 +22,6 @@ import DeployService from 'server/services/deploy'; import { DeployStatus } from 'shared/constants'; import { nanoid } from 'nanoid'; import BuildService from 'server/services/build'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'builds/[uuid]/services/[name]/build.ts', @@ -38,8 +37,6 @@ const logger = rootLogger.child({ * will be queued for deployment and its status will be updated accordingly. * tags: * - Services - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -104,9 +101,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid, name } = req.query; try { diff --git a/src/pages/api/v1/builds/[uuid]/services/[name]/buildLogs.ts b/src/pages/api/v1/builds/[uuid]/services/[name]/buildLogs.ts index f4b2599c..32476b75 100644 --- a/src/pages/api/v1/builds/[uuid]/services/[name]/buildLogs.ts +++ b/src/pages/api/v1/builds/[uuid]/services/[name]/buildLogs.ts @@ -18,7 +18,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import rootLogger from 'server/lib/logger'; import * as k8s from '@kubernetes/client-node'; import { HttpError } from '@kubernetes/client-node'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: __filename, @@ -164,8 +163,6 @@ async function getNativeBuildJobs(serviceName: string, namespace: string): Promi * tags: * - Builds * - Native Build - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -279,9 +276,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid, name } = req.query; if (typeof uuid !== 'string' || typeof name !== 'string') { diff --git a/src/pages/api/v1/builds/[uuid]/services/[name]/buildLogs/[jobName].ts b/src/pages/api/v1/builds/[uuid]/services/[name]/buildLogs/[jobName].ts index a7033894..0c499ff4 100644 --- a/src/pages/api/v1/builds/[uuid]/services/[name]/buildLogs/[jobName].ts +++ b/src/pages/api/v1/builds/[uuid]/services/[name]/buildLogs/[jobName].ts @@ -17,7 +17,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import rootLogger from 'server/lib/logger'; import unifiedLogStreamHandler from '../logs/[jobName]'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'buildLogs/[jobName].ts', @@ -35,8 +34,6 @@ const logger = rootLogger.child({ * tags: * - Builds * - Native Build - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -104,9 +101,6 @@ const logger = rootLogger.child({ * description: Internal server error */ export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { valid } = await validateAuth(req, res); - if (!valid) return; - logger.info( `method=${req.method} jobName=${req.query.jobName} message="Build logs endpoint called, delegating to unified handler"` ); diff --git a/src/pages/api/v1/builds/[uuid]/services/[name]/deployLogs.ts b/src/pages/api/v1/builds/[uuid]/services/[name]/deployLogs.ts index 1dfdb306..e15cc2e7 100644 --- a/src/pages/api/v1/builds/[uuid]/services/[name]/deployLogs.ts +++ b/src/pages/api/v1/builds/[uuid]/services/[name]/deployLogs.ts @@ -18,7 +18,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import rootLogger from 'server/lib/logger'; import * as k8s from '@kubernetes/client-node'; import { HttpError } from '@kubernetes/client-node'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: __filename, @@ -175,8 +174,6 @@ async function getDeploymentJobs(serviceName: string, namespace: string): Promis * This includes both Helm deployment jobs and GitHub-type deployment jobs. * tags: * - Deployments - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -258,9 +255,6 @@ const deployLogsHandler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid, name } = req.query; if (typeof uuid !== 'string' || typeof name !== 'string') { diff --git a/src/pages/api/v1/builds/[uuid]/services/[name]/deployLogs/[jobName].ts b/src/pages/api/v1/builds/[uuid]/services/[name]/deployLogs/[jobName].ts index bdbe5733..0750189f 100644 --- a/src/pages/api/v1/builds/[uuid]/services/[name]/deployLogs/[jobName].ts +++ b/src/pages/api/v1/builds/[uuid]/services/[name]/deployLogs/[jobName].ts @@ -26,8 +26,6 @@ * tags: * - Deployments * - Native Helm - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -135,16 +133,12 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import rootLogger from 'server/lib/logger'; import unifiedLogStreamHandler from '../logs/[jobName]'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: __filename, }); const deployLogStreamHandler = async (req: NextApiRequest, res: NextApiResponse) => { - const { valid } = await validateAuth(req, res); - if (!valid) return; - logger.info( `method=${req.method} jobName=${req.query.jobName} message="Deploy logs endpoint called, delegating to unified handler"` ); diff --git a/src/pages/api/v1/builds/[uuid]/services/[name]/deployment.ts b/src/pages/api/v1/builds/[uuid]/services/[name]/deployment.ts index 344da28f..a6b3f96e 100644 --- a/src/pages/api/v1/builds/[uuid]/services/[name]/deployment.ts +++ b/src/pages/api/v1/builds/[uuid]/services/[name]/deployment.ts @@ -19,7 +19,6 @@ import rootLogger from 'server/lib/logger'; import * as k8s from '@kubernetes/client-node'; import { HttpError } from '@kubernetes/client-node'; import { Deploy } from 'server/models'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: __filename, @@ -159,8 +158,6 @@ async function getGitHubDeploymentDetails( * For GitHub-type deployments, this includes the Kubernetes manifest. * tags: * - Deployments - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -256,9 +253,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid, name } = req.query; if (typeof uuid !== 'string' || typeof name !== 'string') { diff --git a/src/pages/api/v1/builds/[uuid]/services/[name]/logs.ts b/src/pages/api/v1/builds/[uuid]/services/[name]/logs.ts index 62997183..339e94c8 100644 --- a/src/pages/api/v1/builds/[uuid]/services/[name]/logs.ts +++ b/src/pages/api/v1/builds/[uuid]/services/[name]/logs.ts @@ -19,7 +19,6 @@ import { spawn, exec } from 'child_process'; import { promisify } from 'util'; import GithubService from 'server/services/github'; import { Build } from 'server/models'; -import { validateAuth } from 'server/lib/auth/validate'; // Constants const MAX_CONCURRENT_PODS = 5; @@ -54,8 +53,6 @@ const isValidContainerType = (type: string | undefined): type is ContainerType = * deprecated: true * tags: * - Logs - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -175,9 +172,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return; } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid, name } = req.query; if (!uuid || !name || typeof uuid !== 'string' || typeof name !== 'string') { res.status(400).json({ error: 'Invalid path parameters' }); diff --git a/src/pages/api/v1/builds/[uuid]/services/[name]/logs/[jobName].ts b/src/pages/api/v1/builds/[uuid]/services/[name]/logs/[jobName].ts index b3c56997..6d85e81d 100644 --- a/src/pages/api/v1/builds/[uuid]/services/[name]/logs/[jobName].ts +++ b/src/pages/api/v1/builds/[uuid]/services/[name]/logs/[jobName].ts @@ -27,8 +27,6 @@ * - Logs * - Builds * - Deployments - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -165,7 +163,6 @@ import rootLogger from 'server/lib/logger'; import { getK8sJobStatusAndPod } from 'server/lib/logStreamingHelper'; import BuildService from 'server/services/build'; import { HttpError } from '@kubernetes/client-node'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: __filename, @@ -232,9 +229,6 @@ const unifiedLogStreamHandler = async (req: NextApiRequest, res: NextApiResponse return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { uuid, name, jobName, type } = req.query; // For webhook jobs, name can be undefined diff --git a/src/pages/api/v1/builds/[uuid]/torndown.ts b/src/pages/api/v1/builds/[uuid]/torndown.ts index ad468d3f..d9991d72 100644 --- a/src/pages/api/v1/builds/[uuid]/torndown.ts +++ b/src/pages/api/v1/builds/[uuid]/torndown.ts @@ -20,7 +20,6 @@ import { Build } from 'server/models'; import { BuildStatus, DeployStatus } from 'shared/constants'; import BuildService from 'server/services/build'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'builds/[uuid]/torndown.ts', @@ -36,8 +35,6 @@ const logger = rootLogger.child({ * UUID to torn_down. This effectively marks the environment as deleted. * tags: * - Builds - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -108,9 +105,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const uuid = req.query?.uuid; try { diff --git a/src/pages/api/v1/builds/[uuid]/webhooks.ts b/src/pages/api/v1/builds/[uuid]/webhooks.ts index e2d19142..1f7c2e78 100644 --- a/src/pages/api/v1/builds/[uuid]/webhooks.ts +++ b/src/pages/api/v1/builds/[uuid]/webhooks.ts @@ -19,7 +19,6 @@ import rootLogger from 'server/lib/logger'; import GithubService from 'server/services/github'; import { Build } from 'server/models'; import WebhookService from 'server/services/webhook'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'builds/[uuid]/webhooks.ts', @@ -36,8 +35,6 @@ const logger = rootLogger.child({ * tags: * - Webhooks * - Builds - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -118,8 +115,6 @@ const logger = rootLogger.child({ * tags: * - Webhooks * - Builds - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: uuid @@ -203,9 +198,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(400).json({ error: 'Invalid UUID' }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - try { switch (req.method) { case 'GET': diff --git a/src/pages/api/v1/builds/index.ts b/src/pages/api/v1/builds/index.ts index 325dcd43..671c6e80 100644 --- a/src/pages/api/v1/builds/index.ts +++ b/src/pages/api/v1/builds/index.ts @@ -17,7 +17,6 @@ import { NextApiRequest, NextApiResponse } from 'next/types'; import rootLogger from 'server/lib/logger'; import BuildService from 'server/services/build'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'api/v1/builds/index.ts', @@ -33,8 +32,6 @@ const logger = rootLogger.child({ * By default, excludes builds with status 'torn_down' and 'pending'. * tags: * - Builds - * security: - * - ApiKeyAuth: [] * parameters: * - in: query * name: exclude @@ -143,9 +140,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - try { const buildService = new BuildService(); diff --git a/src/pages/api/v1/config/cache.ts b/src/pages/api/v1/config/cache.ts index a85ae9da..2eef2b97 100644 --- a/src/pages/api/v1/config/cache.ts +++ b/src/pages/api/v1/config/cache.ts @@ -17,7 +17,6 @@ import { NextApiRequest, NextApiResponse } from 'next'; import rootLogger from 'server/lib/logger'; import GlobalConfigService from 'server/services/globalConfig'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'v1/config/cache.ts', @@ -31,8 +30,6 @@ const logger = rootLogger.child({ * description: Fetches the current global configuration values from cache * tags: * - Configuration - * security: - * - ApiKeyAuth: [] * responses: * 200: * description: Successfully retrieved configuration @@ -69,8 +66,6 @@ const logger = rootLogger.child({ * description: Forces a refresh of the cached configuration values and returns the updated configuration * tags: * - Configuration - * security: - * - ApiKeyAuth: [] * responses: * 200: * description: Successfully refreshed and retrieved configuration @@ -105,9 +100,6 @@ const logger = rootLogger.child({ */ // eslint-disable-next-line import/no-anonymous-default-export export default async (req: NextApiRequest, res: NextApiResponse) => { - const { valid } = await validateAuth(req, res); - if (!valid) return; - try { switch (req.method) { case 'GET': diff --git a/src/pages/api/v1/deploy-summary.ts b/src/pages/api/v1/deploy-summary.ts index d67a3466..06cc692c 100644 --- a/src/pages/api/v1/deploy-summary.ts +++ b/src/pages/api/v1/deploy-summary.ts @@ -17,7 +17,6 @@ import { NextApiRequest, NextApiResponse } from 'next/types'; import rootLogger from 'server/lib/logger'; import BuildService from 'server/services/build'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'deploy-summary.ts', @@ -32,8 +31,6 @@ const logger = rootLogger.child({ * Retrieves deploy summary information from the deploySummary view for a specific build ID. * tags: * - Deploys - * security: - * - ApiKeyAuth: [] * parameters: * - in: query * name: buildId @@ -119,9 +116,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { buildId } = req.query; const parsedBuildId = parseInt(buildId as string, 10); diff --git a/src/pages/api/v1/deployables.ts b/src/pages/api/v1/deployables.ts index 9fc9cb35..dd4d5a04 100644 --- a/src/pages/api/v1/deployables.ts +++ b/src/pages/api/v1/deployables.ts @@ -17,7 +17,6 @@ import { NextApiRequest, NextApiResponse } from 'next/types'; import rootLogger from 'server/lib/logger'; import BuildService from 'server/services/build'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'deployables.ts', @@ -32,8 +31,6 @@ const logger = rootLogger.child({ * Retrieves all deployables associated with a specific build ID. * tags: * - Deployables - * security: - * - ApiKeyAuth: [] * parameters: * - in: query * name: buildId @@ -121,9 +118,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { buildId, name } = req.query; const parsedBuildId = parseInt(buildId as string, 10); diff --git a/src/pages/api/v1/deploys.ts b/src/pages/api/v1/deploys.ts index fd461fbd..e122f9cf 100644 --- a/src/pages/api/v1/deploys.ts +++ b/src/pages/api/v1/deploys.ts @@ -17,7 +17,6 @@ import { NextApiRequest, NextApiResponse } from 'next/types'; import rootLogger from 'server/lib/logger'; import BuildService from 'server/services/build'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'api/v1/deploys.ts', @@ -32,8 +31,6 @@ const logger = rootLogger.child({ * Retrieves all deploys associated with a specific build ID. * tags: * - Deploys - * security: - * - ApiKeyAuth: [] * parameters: * - in: query * name: buildId @@ -161,9 +158,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { buildId, deployableId } = req.query; const parsedBuildId = parseInt(buildId as string, 10); const parsedDeployableId = deployableId ? parseInt(deployableId as string, 10) : undefined; diff --git a/src/pages/api/v1/pull-requests/[id]/builds.ts b/src/pages/api/v1/pull-requests/[id]/builds.ts index 657f1898..222e5210 100644 --- a/src/pages/api/v1/pull-requests/[id]/builds.ts +++ b/src/pages/api/v1/pull-requests/[id]/builds.ts @@ -18,7 +18,6 @@ import { NextApiRequest, NextApiResponse } from 'next/types'; import rootLogger from 'server/lib/logger'; import BuildService from 'server/services/build'; import PullRequestService from 'server/services/pullRequest'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'pull-requests/[id]/builds.ts', @@ -34,8 +33,6 @@ const logger = rootLogger.child({ * tags: * - Builds * - Pull Requests - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: id @@ -121,9 +118,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { id } = req.query; const parsedId = parseInt(id as string, 10); diff --git a/src/pages/api/v1/pull-requests/[id]/index.ts b/src/pages/api/v1/pull-requests/[id]/index.ts index 4be3b81d..e301801a 100644 --- a/src/pages/api/v1/pull-requests/[id]/index.ts +++ b/src/pages/api/v1/pull-requests/[id]/index.ts @@ -17,7 +17,6 @@ import { NextApiRequest, NextApiResponse } from 'next/types'; import rootLogger from 'server/lib/logger'; import PullRequestService from 'server/services/pullRequest'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'api/v1/pull-requests/[id].ts', @@ -32,8 +31,6 @@ const logger = rootLogger.child({ * Retrieves detailed information about a specific pull request by its ID. * tags: * - Pull Requests - * security: - * - ApiKeyAuth: [] * parameters: * - in: path * name: id @@ -115,9 +112,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - const { id } = req.query; const parsedId = parseInt(id as string, 10); diff --git a/src/pages/api/v1/pull-requests/index.ts b/src/pages/api/v1/pull-requests/index.ts index 6bac75be..908cf060 100644 --- a/src/pages/api/v1/pull-requests/index.ts +++ b/src/pages/api/v1/pull-requests/index.ts @@ -17,7 +17,6 @@ import { NextApiRequest, NextApiResponse } from 'next/types'; import rootLogger from 'server/lib/logger'; import PullRequestService from 'server/services/pullRequest'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'api/v1/pull-requests/index.ts', @@ -33,8 +32,6 @@ const logger = rootLogger.child({ * Results are ordered by updatedAt descending and paginated with a default limit of 25. * tags: * - Pull Requests - * security: - * - ApiKeyAuth: [] * parameters: * - in: query * name: user @@ -185,9 +182,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - try { const pullRequestService = new PullRequestService(); diff --git a/src/pages/api/v1/repos/index.ts b/src/pages/api/v1/repos/index.ts index 50f5ae88..55d0b797 100644 --- a/src/pages/api/v1/repos/index.ts +++ b/src/pages/api/v1/repos/index.ts @@ -17,7 +17,6 @@ import { NextApiRequest, NextApiResponse } from 'next/types'; import rootLogger from 'server/lib/logger'; import PullRequestService from 'server/services/pullRequest'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'api/v1/repos/index.ts', @@ -33,8 +32,6 @@ const logger = rootLogger.child({ * Returns all repositories by default, with optional pagination support. * tags: * - Repositories - * security: - * - ApiKeyAuth: [] * parameters: * - in: query * name: page @@ -118,9 +115,6 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - try { const pullRequestService = new PullRequestService(); diff --git a/src/pages/api/v1/schema/validate.ts b/src/pages/api/v1/schema/validate.ts index 375f81da..29d08afe 100644 --- a/src/pages/api/v1/schema/validate.ts +++ b/src/pages/api/v1/schema/validate.ts @@ -22,8 +22,6 @@ * description: Validates a YAML config provided as content or fetched from a repo/branch. * tags: * - Schema - * security: - * - ApiKeyAuth: [] * requestBody: * required: true * content: @@ -108,7 +106,6 @@ import { getYamlFileContentFromBranch } from 'server/lib/github'; import rootLogger from 'server/lib/logger'; import { YamlConfigParser, ParsingError } from 'server/lib/yamlConfigParser'; import { YamlConfigValidator, ValidationError } from 'server/lib/yamlConfigValidator'; -import { validateAuth } from 'server/lib/auth/validate'; const logger = rootLogger.child({ filename: 'v1/schema/validate', @@ -119,9 +116,6 @@ const schemaValidateHandler = async (req: NextApiRequest, res: NextApiResponse { return res.status(405).json({ error: `${req.method} is not allowed` }); } - const { valid } = await validateAuth(req, res); - if (!valid) return; - try { const pullRequestService = new PullRequestService(); diff --git a/src/pages/v1/docs.tsx b/src/pages/v1/docs.tsx index db8e9300..d71fd44d 100644 --- a/src/pages/v1/docs.tsx +++ b/src/pages/v1/docs.tsx @@ -47,16 +47,6 @@ export const getServerSideProps: GetServerSideProps = async () => { version: '1.0.0', description: 'API documentation for lifecycle', }, - components: { - securitySchemes: { - ApiKeyAuth: { - type: 'http', - scheme: 'bearer', - bearerFormat: 'API Key', - description: 'API key authentication using `Bearer API_KEY`', - }, - }, - }, }, // Adjust this glob pattern to match your API files apis: ['./src/pages/api/**/*.ts', './ws-server.ts'], diff --git a/src/server/db/migrations/003_add_api_keys.ts b/src/server/db/migrations/003_add_api_keys.ts deleted file mode 100644 index bd51a7de..00000000 --- a/src/server/db/migrations/003_add_api_keys.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Knex } from 'knex'; - -export async function up(knex: Knex): Promise { - await knex.schema.createTable('api_keys', (table) => { - table.increments('id').primary(); - table.string('key_id', 8).notNullable().unique(); - table.string('secret_hash', 255).notNullable(); - table.string('name', 255).notNullable(); - table.text('description'); - table.boolean('active').defaultTo(true); - table.jsonb('scopes').defaultTo('[]'); - table.bigint('github_user_id'); - table.string('github_login', 255); - table.timestamp('created_at').defaultTo(knex.fn.now()); - table.timestamp('updated_at').defaultTo(knex.fn.now()); - table.timestamp('expires_at'); - table.timestamp('last_used_at'); - - // Indexes for performance - table.index('key_id', 'idx_api_keys_key_id'); - table.index('active', 'idx_api_keys_active'); - table.index('expires_at', 'idx_api_keys_expires_at'); - table.index('last_used_at', 'idx_api_keys_last_used_at'); - table.index('github_user_id', 'idx_api_keys_github_user_id'); - table.index('github_login', 'idx_api_keys_github_login'); - }); -} - -export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists('api_keys'); -} diff --git a/src/server/lib/auth/constants.ts b/src/server/lib/auth/constants.ts deleted file mode 100644 index e89e6557..00000000 --- a/src/server/lib/auth/constants.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * API Key format constants - */ -export const API_KEY_PREFIX = 'lfc_'; -export const API_KEY_REGEX = /^lfc_([A-Za-z0-9_-]{8})_([A-Za-z0-9_-]{32})$/; -export const SECRET_BYTES = 24; // 24 bytes = 32 chars base64url diff --git a/src/server/lib/auth/keyGenerator.ts b/src/server/lib/auth/keyGenerator.ts deleted file mode 100644 index b397c118..00000000 --- a/src/server/lib/auth/keyGenerator.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import crypto from 'crypto'; -import bcrypt from 'bcryptjs'; -import { API_KEY_PREFIX, API_KEY_REGEX, SECRET_BYTES } from './constants'; - -export interface GeneratedApiKey { - fullKey: string; - keyId: string; - secret: string; - secretHash: string; -} - -/** - * Generate base64url-encoded string from random bytes - */ -function generateBase64Url(bytes: Buffer): string { - return bytes.toString('base64url'); -} - -/** - * Generate a new API key with the format: lfc__ - * Returns the full key (shown once to user) and components for storage - */ -export async function generateApiKey(bcryptRounds = 12): Promise { - // Generate key ID (8 chars base64url from 6 bytes) - const keyIdBytes = crypto.randomBytes(6); - const keyId = generateBase64Url(keyIdBytes); - - // Generate secret (32 chars base64url from 24 bytes) - const secretBytes = crypto.randomBytes(SECRET_BYTES); - const secret = generateBase64Url(secretBytes); - - // Create full key - const fullKey = `${API_KEY_PREFIX}${keyId}_${secret}`; - - // Hash the secret portion for storage - const secretHash = await bcrypt.hash(secret, bcryptRounds); - - return { - fullKey, - keyId, - secret, - secretHash, - }; -} - -/** - * Parse an API key into its components - * Returns null if the format is invalid - */ -export function parseApiKey(apiKey: string): { keyId: string; secret: string } | null { - const match = apiKey.match(API_KEY_REGEX); - - if (!match) { - return null; - } - - const [, keyId, secret] = match; - return { keyId, secret }; -} - -/** - * Validate an API key secret against a stored hash - */ -export async function validateSecret(secret: string, storedHash: string): Promise { - return bcrypt.compare(secret, storedHash); -} diff --git a/src/server/lib/auth/rateLimiter.ts b/src/server/lib/auth/rateLimiter.ts deleted file mode 100644 index eda0a417..00000000 --- a/src/server/lib/auth/rateLimiter.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { RedisClient } from '../redisClient'; - -export interface RateLimitResult { - allowed: boolean; - limit: number; - remaining: number; - resetAt: number; -} - -/** - * Check and update rate limit using fixed window algorithm - * @param keyId API key ID - * @param limit Maximum requests per window - * @param windowSeconds Window duration in seconds - */ -export async function checkRateLimit(keyId: string, limit: number, windowSeconds: number): Promise { - const now = Date.now(); - const windowStart = Math.floor(now / (windowSeconds * 1000)); - const resetAt = (windowStart + 1) * windowSeconds * 1000; - - // Redis key for this time window - const redisKey = `ratelimit:${keyId}:${windowStart}`; - - const redis = RedisClient.getInstance(); - const client = redis.getRedis(); - - // Increment counter and get new value - const count = await client.incr(redisKey); - - // Set TTL on first request in this window - if (count === 1) { - await client.expire(redisKey, windowSeconds); - } - - const remaining = Math.max(0, limit - count); - const allowed = count <= limit; - - return { - allowed, - limit, - remaining, - resetAt, - }; -} diff --git a/src/server/lib/auth/validate.ts b/src/server/lib/auth/validate.ts deleted file mode 100644 index 9fdf9eaf..00000000 --- a/src/server/lib/auth/validate.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { NextApiRequest, NextApiResponse } from 'next'; -import AuthService from '../../services/auth'; -import { checkRateLimit } from './rateLimiter'; -import ApiKey from '../../models/ApiKey'; -import rootLogger from '../logger'; - -const logger = rootLogger.child({ - filename: 'lib/auth/validateAuth.ts', -}); - -/** - * Validate API key and rate limits - * This is called from within API route handlers after the edge middleware - * has done basic format validation - */ -export async function validateAuth( - req: NextApiRequest, - res: NextApiResponse -): Promise<{ valid: boolean; apiKey?: ApiKey }> { - try { - const authService = new AuthService(); - const config = await authService.getApiConfig(); - - // Skip all authentication and rate limiting if requireAuth is false - if (!config.requireAuth) { - return { valid: true }; - } - - const authHeader = req.headers.authorization as string; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - logger.error('Missing or invalid Authorization header'); - res.status(401).json({ error: 'Unauthorized' }); - return { valid: false }; - } - - const apiKeyString = authHeader.substring(7); // Remove 'Bearer ' prefix - - const apiKey = await authService.validateApiKey(apiKeyString); - - if (!apiKey) { - logger.error('Invalid API key provided'); - res.status(401).json({ error: 'Unauthorized' }); - return { valid: false }; - } - - // Check rate limits - const rateLimitResult = await checkRateLimit(apiKey.keyId, config.rate_limit, config.rate_limit_window); - - res.setHeader('X-RateLimit-Limit', rateLimitResult.limit.toString()); - res.setHeader('X-RateLimit-Remaining', rateLimitResult.remaining.toString()); - res.setHeader('X-RateLimit-Reset', new Date(rateLimitResult.resetAt).toISOString()); - - if (!rateLimitResult.allowed) { - logger.error('Rate limit exceeded for API key', { keyId: apiKey.keyId }); - res.status(429).json({ error: 'Too Many Requests' }); - return { valid: false }; - } - - // Update last used timestamp (async, non-blocking) - await authService.updateLastUsed(apiKey); - - return { valid: true, apiKey }; - } catch (error) { - logger.error('API key validation error:', error); - res.status(500).json({ error: 'Internal Server Error' }); - return { valid: false }; - } -} diff --git a/src/server/models/ApiKey.ts b/src/server/models/ApiKey.ts deleted file mode 100644 index 2c8b64ec..00000000 --- a/src/server/models/ApiKey.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Model from './_Model'; - -export default class ApiKey extends Model { - static tableName = 'api_keys'; - - id!: number; - keyId!: string; - secretHash!: string; - name!: string; - description?: string; - active!: boolean; - scopes!: string[]; - githubUserId?: number; - githubLogin?: string; - createdAt!: string; - updatedAt!: string; - expiresAt?: string; - lastUsedAt?: string; - - static get jsonSchema() { - return { - type: 'object', - required: ['keyId', 'secretHash', 'name'], - properties: { - id: { type: 'integer' }, - keyId: { type: 'string', minLength: 8, maxLength: 8 }, - secretHash: { type: 'string', maxLength: 255 }, - name: { type: 'string', maxLength: 255 }, - description: { type: 'string' }, - active: { type: 'boolean' }, - scopes: { type: 'array', items: { type: 'string' } }, - githubUserId: { type: ['integer', 'null'] }, - githubLogin: { type: ['string', 'null'], maxLength: 255 }, - createdAt: { type: 'string' }, - updatedAt: { type: 'string' }, - expiresAt: { type: ['string', 'null'] }, - lastUsedAt: { type: ['string', 'null'] }, - }, - }; - } - - static columnNameMappers = { - parse(obj: any) { - return { - ...obj, - keyId: obj.key_id, - secretHash: obj.secret_hash, - githubUserId: obj.github_user_id, - githubLogin: obj.github_login, - createdAt: obj.created_at, - updatedAt: obj.updated_at, - expiresAt: obj.expires_at, - lastUsedAt: obj.last_used_at, - }; - }, - format(obj: any) { - const formatted: any = { ...obj }; - if ('keyId' in formatted) { - formatted.key_id = formatted.keyId; - delete formatted.keyId; - } - if ('secretHash' in formatted) { - formatted.secret_hash = formatted.secretHash; - delete formatted.secretHash; - } - if ('githubUserId' in formatted) { - formatted.github_user_id = formatted.githubUserId; - delete formatted.githubUserId; - } - if ('githubLogin' in formatted) { - formatted.github_login = formatted.githubLogin; - delete formatted.githubLogin; - } - if ('createdAt' in formatted) { - formatted.created_at = formatted.createdAt; - delete formatted.createdAt; - } - if ('updatedAt' in formatted) { - formatted.updated_at = formatted.updatedAt; - delete formatted.updatedAt; - } - if ('expiresAt' in formatted) { - formatted.expires_at = formatted.expiresAt; - delete formatted.expiresAt; - } - if ('lastUsedAt' in formatted) { - formatted.last_used_at = formatted.lastUsedAt; - delete formatted.lastUsedAt; - } - return formatted; - }, - }; -} diff --git a/src/server/services/auth.ts b/src/server/services/auth.ts deleted file mode 100644 index cca412d9..00000000 --- a/src/server/services/auth.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Service from './_service'; -import ApiKey from '../models/ApiKey'; -import GlobalConfigService from './globalConfig'; -import { generateApiKey, parseApiKey, validateSecret } from '../lib/auth/keyGenerator'; -import { ApiConfig } from './types/globalConfig'; -import rootLogger from '../lib/logger'; - -const logger = rootLogger.child({ - filename: 'services/auth.ts', -}); - -const DEFAULT_API_CONFIG: ApiConfig = { - rate_limit: 1000, - rate_limit_window: 600, - bcrypt_rounds: 12, - requireAuth: false, -}; - -export interface CreateApiKeyOptions { - name: string; - description?: string; - githubUserId?: number; - githubLogin?: string; - scopes?: string[]; -} - -export default class AuthService extends Service { - /** - * Check if any API keys exist in the database - */ - async hasApiKeys(): Promise { - const result = await ApiKey.query().select('id').limit(1).first(); - return Boolean(result); - } - - /** - * Get API configuration from global_config table using GlobalConfigService cache - */ - async getApiConfig(): Promise { - try { - const globalConfigService = GlobalConfigService.getInstance(); - const allConfigs = await globalConfigService.getAllConfigs(); - - return { ...DEFAULT_API_CONFIG, ...allConfigs?.apiConfig }; - } catch (error) { - logger.error('Failed to fetch API config, using defaults:', error); - return DEFAULT_API_CONFIG; - } - } - - /** - * Create a new API key - */ - async createApiKey(options: CreateApiKeyOptions): Promise<{ apiKey: ApiKey; fullKey: string }> { - const config = await this.getApiConfig(); - const generated = await generateApiKey(config.bcrypt_rounds); - - const apiKey = await ApiKey.query().insert({ - keyId: generated.keyId, - secretHash: generated.secretHash, - name: options.name, - description: options.description, - githubUserId: options.githubUserId, - githubLogin: options.githubLogin, - scopes: options.scopes || [], - active: true, - }); - - return { - apiKey, - fullKey: generated.fullKey, // Only returned on creation, never again - }; - } - - /** - * Validate an API key and return the key record if valid - */ - async validateApiKey(apiKeyString: string): Promise { - const parsed = parseApiKey(apiKeyString); - if (!parsed) { - return null; - } - - const { keyId, secret } = parsed; - - const apiKey = await ApiKey.query().where('key_id', keyId).where('active', true).first(); - - if (!apiKey) { - return null; - } - - const isValid = await validateSecret(secret, apiKey.secretHash); - if (!isValid) { - return null; - } - - return apiKey; - } - - /** - * Update last used timestamp (throttled) - */ - async updateLastUsed(apiKey: ApiKey): Promise { - const THRESHOLD = 5 * 60 * 1000; // 5 minutes - const now = new Date(); - - if (!apiKey.lastUsedAt || now.getTime() - new Date(apiKey.lastUsedAt).getTime() > THRESHOLD) { - // Async update - don't await - setImmediate(async () => { - try { - await ApiKey.query().where('id', apiKey.id).patch({ lastUsedAt: now.toISOString() }); - } catch (error) { - logger.error('Failed to update last_used_at:', error); - } - }); - } - } - - /** - * List all API keys (masked) - */ - async listApiKeys(): Promise { - return ApiKey.query() - .select( - 'id', - 'key_id', - 'name', - 'description', - 'active', - 'github_user_id', - 'github_login', - 'created_at', - 'updated_at', - 'last_used_at' - ) - .orderBy('created_at', 'desc'); - } - - /** - * Revoke an API key - */ - async revokeApiKey(id: number): Promise { - const result = await ApiKey.query().where('id', id).patch({ active: false }); - - return result > 0; - } - - /** - * Update an API key - */ - async updateApiKey(id: number, updates: Partial): Promise { - const apiKey = await ApiKey.query().patchAndFetchById(id, { - name: updates.name, - description: updates.description, - scopes: updates.scopes, - }); - - return apiKey; - } -} diff --git a/src/server/services/types/globalConfig.ts b/src/server/services/types/globalConfig.ts index 0f5dd522..7aa623e5 100644 --- a/src/server/services/types/globalConfig.ts +++ b/src/server/services/types/globalConfig.ts @@ -37,7 +37,6 @@ export type GlobalConfig = { features: Record; app_setup: AppSetup; labels: LabelsConfig; - apiConfig?: ApiConfig; }; export type AppSetup = { @@ -147,10 +146,3 @@ export type LabelsConfig = { defaultStatusComments: boolean; defaultControlComments: boolean; }; - -export type ApiConfig = { - rate_limit: number; - rate_limit_window: number; - bcrypt_rounds: number; - requireAuth: boolean; -}; diff --git a/swaggerSpec.ts b/swaggerSpec.ts index 7a11eb16..f2a18686 100644 --- a/swaggerSpec.ts +++ b/swaggerSpec.ts @@ -24,16 +24,6 @@ const options = { version: '1.0.0', description: 'API documentation for lifecycle', }, - components: { - securitySchemes: { - ApiKeyAuth: { - type: 'http', - scheme: 'bearer', - bearerFormat: 'API Key', - description: 'API key authentication using `Bearer API_KEY`', - }, - }, - }, }, apis: ['./src/pages/api/**/*.ts'], };