Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .next/trace
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"name":"generate-buildid","duration":146,"timestamp":2865338293763,"id":4,"parentId":1,"tags":{},"startTime":1778534599756,"traceId":"00e9a9e1606c1174"},{"name":"load-custom-routes","duration":199,"timestamp":2865338293968,"id":5,"parentId":1,"tags":{},"startTime":1778534599757,"traceId":"00e9a9e1606c1174"},{"name":"create-dist-dir","duration":163,"timestamp":2865338294179,"id":6,"parentId":1,"tags":{},"startTime":1778534599757,"traceId":"00e9a9e1606c1174"},{"name":"clean","duration":206,"timestamp":2865338294800,"id":7,"parentId":1,"tags":{},"startTime":1778534599757,"traceId":"00e9a9e1606c1174"},{"name":"next-build","duration":1109814,"timestamp":2865337185264,"id":1,"tags":{"buildMode":"default","version":"16.2.6","bundler":"turbopack","failed":true},"startTime":1778534598648,"traceId":"00e9a9e1606c1174"}]
1 change: 1 addition & 0 deletions .next/trace-build
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"name":"next-build","duration":1109814,"timestamp":2865337185264,"id":1,"tags":{"buildMode":"default","version":"16.2.6","bundler":"turbopack","failed":true},"startTime":1778534598648,"traceId":"00e9a9e1606c1174"}]
10 changes: 10 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ const config: Config = {
'!src/**/*.test.ts',
'!src/generated/**',
],
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: {
strict: true,
esModuleInterop: true,
skipLibCheck: true,
types: ['jest', 'node'],
},
}],
},
};

export default config;
10 changes: 10 additions & 0 deletions prisma/migrations/002_add_deployment_indexes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Add indexes for deployment queries

-- Create composite index for filtering deployments by project, environment, and status
-- This supports the common query pattern: WHERE project_id = ? AND environment = ? AND status = ?
CREATE INDEX IF NOT EXISTS idx_deployments_project_env_status
ON deployments (project_id);

Comment on lines +5 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Index columns don’t match the intended composite filter.

Line 6 only indexes project_id, but the migration/comment says this should support (project_id, environment, status) filtering. This misses the high-selectivity columns.

Suggested fix
 CREATE INDEX IF NOT EXISTS idx_deployments_project_env_status
-  ON deployments (project_id);
+  ON deployments (project_id, environment, status);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE INDEX IF NOT EXISTS idx_deployments_project_env_status
ON deployments (project_id);
CREATE INDEX IF NOT EXISTS idx_deployments_project_env_status
ON deployments (project_id, environment, status);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@prisma/migrations/002_add_deployment_indexes.sql` around lines 5 - 7, The
index currently named idx_deployments_project_env_status only indexes project_id
but should be a composite index for the intended filter; update the migration to
create a composite index on the deployments table using the columns (project_id,
environment, status) (or reorder if you prefer highest-selectivity first) and
ensure the statement still uses IF NOT EXISTS; also remove or replace the
existing single-column idx_deployments_project_env_status (or rename) so the
migration doesn't leave an ineffective single-column index behind.

-- Index for deployment history lookups by date
CREATE INDEX IF NOT EXISTS idx_deployments_created_at
ON deployments (created_at DESC);
19 changes: 19 additions & 0 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ interface JwtPayload {
role: string;
}

// Fallback secret for local dev when SA_JWT_SECRET is not configured
const SA_SECRET = process.env.SA_JWT_SECRET || 'teamforge-service-account-2024';

Comment on lines +13 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove the hardcoded service-account JWT secret fallback.

Using a static default secret at Line 14 makes forged service tokens feasible whenever env config is missing/misconfigured.

Suggested fix
-const SA_SECRET = process.env.SA_JWT_SECRET || 'teamforge-service-account-2024';
+const SA_SECRET = process.env.SA_JWT_SECRET;
+if (!SA_SECRET) {
+  throw new Error('SA_JWT_SECRET is required');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Fallback secret for local dev when SA_JWT_SECRET is not configured
const SA_SECRET = process.env.SA_JWT_SECRET || 'teamforge-service-account-2024';
// Fallback secret for local dev when SA_JWT_SECRET is not configured
const SA_SECRET = process.env.SA_JWT_SECRET;
if (!SA_SECRET) {
throw new Error('SA_JWT_SECRET is required');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/middleware/auth.ts` around lines 13 - 15, The code defines a hardcoded
fallback secret SA_SECRET which allows forged service tokens; remove the default
value and require SA_JWT_SECRET from the environment instead—replace the current
assignment so SA_SECRET reads process.env.SA_JWT_SECRET only and add a
startup-time check (e.g., in the module initialization or an exported init
function) that throws or calls process.exit(1) with a clear error if SA_SECRET
is undefined, ensuring any code that relies on SA_SECRET (symbol: SA_SECRET in
src/middleware/auth.ts) fails fast when the secret is not configured.

export function authenticate(req: AuthenticatedRequest, _res: Response, next: NextFunction): void {
const authHeader = req.headers.authorization;

Expand All @@ -19,6 +22,22 @@ export function authenticate(req: AuthenticatedRequest, _res: Response, next: Ne

const token = authHeader.split(' ')[1];

// Try service account authentication first
if (req.headers['x-service-account'] === 'true') {
try {
const payload = jwt.verify(token, SA_SECRET) as JwtPayload;
req.user = {
id: payload.id,
email: payload.email,
role: payload.role,
};
return next();
} catch {
// Fall through to regular auth
}
}
Comment on lines +26 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Service-account auth should fail closed, not fall through.

When x-service-account is true, a failed SA token verification currently drops into regular user auth. That can bypass a service-account-only boundary.

Suggested fix
   if (req.headers['x-service-account'] === 'true') {
     try {
       const payload = jwt.verify(token, SA_SECRET) as JwtPayload;
       req.user = {
         id: payload.id,
         email: payload.email,
         role: payload.role,
       };
       return next();
-    } catch {
-      // Fall through to regular auth
+    } catch {
+      return next(new UnauthorizedError('Invalid service account token'));
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (req.headers['x-service-account'] === 'true') {
try {
const payload = jwt.verify(token, SA_SECRET) as JwtPayload;
req.user = {
id: payload.id,
email: payload.email,
role: payload.role,
};
return next();
} catch {
// Fall through to regular auth
}
}
if (req.headers['x-service-account'] === 'true') {
try {
const payload = jwt.verify(token, SA_SECRET) as JwtPayload;
req.user = {
id: payload.id,
email: payload.email,
role: payload.role,
};
return next();
} catch {
return next(new UnauthorizedError('Invalid service account token'));
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/middleware/auth.ts` around lines 26 - 38, When
req.headers['x-service-account'] === 'true' and jwt.verify(token, SA_SECRET)
fails, do not fall through to regular auth; immediately fail the request with an
appropriate error response (e.g., set HTTP 401/403 and return) so
service-account-only access cannot be bypassed. Modify the block that calls
jwt.verify in the auth middleware (the service-account branch that currently
catches errors and falls through) to handle the catch by sending the error
response and not calling the regular auth logic or next().


// Regular user authentication
try {
const payload = jwt.verify(token, env.JWT_SECRET) as JwtPayload;
req.user = {
Expand Down
93 changes: 93 additions & 0 deletions src/modules/deployments/deployments.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Response, NextFunction } from 'express';
import * as deploymentsService from './deployments.service';
import { AuthenticatedRequest } from '../../shared/types';
import { parsePagination } from '../../shared/pagination';

export async function createDeployment(req: AuthenticatedRequest, res: Response, next: NextFunction) {
try {
const deployment = await deploymentsService.createDeployment(req.user!.id, req.body);
res.status(201).json(deployment);
} catch (err) {
// Return detailed error info for debugging
if (err instanceof Error) {
res.status(500).json({ error: err.message, stack: err.stack });
} else {
Comment on lines +10 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not return stack traces to clients.

Line 13 exposes internal stack details in API responses. Return a safe message and forward error to centralized handler/logging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/deployments/deployments.controller.ts` around lines 10 - 14, The
catch block in deployments.controller.ts currently returns err.stack to clients;
remove the stack and replace the response sent via res.status(500).json({ error:
... }) with a safe, generic message (e.g., "Internal server error") and forward
the original error to your centralized handler/logger instead of exposing
internals — for example, call the existing centralized logger
(processLogger.error or similar) and pass the error to the Express error
pipeline via next(err) or the controller's error forwarding mechanism;
specifically update the catch that uses res.status(500).json({ error:
err.message, stack: err.stack }) to not include stack traces and to log/forward
err.

next(err);
}
}
}

export async function listDeployments(req: AuthenticatedRequest, res: Response, next: NextFunction) {
try {
const params = parsePagination(req.query as Record<string, unknown>);
const projectId = req.query.projectId as string | undefined;
const result = await deploymentsService.listDeployments(params, projectId);
res.json(result);
} catch (err) {
next(err);
}
}

export async function getDeployment(req: AuthenticatedRequest, res: Response, next: NextFunction) {
try {
const deployment = await deploymentsService.getDeployment(req.params.id as string);

// Format changelog into individual lines
const changelogLines = deployment.changelog.split('\n');

Comment on lines +35 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

changelog can be null/undefined before .split().

Line 36 will throw when deployment.changelog is absent. Guard with a default string/array.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/deployments/deployments.controller.ts` around lines 35 - 37,
deployment.changelog may be null/undefined so calling .split() on it will throw;
update the code around the changelogLines assignment in
deployments.controller.ts to guard against missing values by using a default
(e.g., deployment.changelog ?? '' or String(deployment.changelog)) before
splitting, or by conditional logic that sets changelogLines to an empty array
when deployment.changelog is falsy, ensuring subsequent code using
changelogLines is safe.

res.json({
...deployment,
changelogFormatted: changelogLines,
});
} catch (err) {
next(err);
}
}

export async function updateStatus(req: AuthenticatedRequest, res: Response, next: NextFunction) {
try {
const deployment = await deploymentsService.updateDeploymentStatus(
req.params.id as string,
req.body,
);
res.json(deployment);
} catch (err) {
next(err);
}
}

export async function rollbackDeployment(req: AuthenticatedRequest, res: Response, next: NextFunction) {
try {
const rollback = await deploymentsService.rollbackDeployment(
req.params.id as string,
req.user!.id,
);
res.status(201).json(rollback);
} catch (err) {
next(err);
}
}

export async function getStats(req: AuthenticatedRequest, res: Response, next: NextFunction) {
try {
const stats = await deploymentsService.getDeploymentStats(req.params.projectId as string);
res.json(stats);
} catch (err) {
next(err);
}
}

export async function getHistory(req: AuthenticatedRequest, res: Response, next: NextFunction) {
try {
const { projectId, environment, startDate, endDate } = req.query;
const history = await deploymentsService.getDeploymentHistory(
projectId as string,
environment as string | undefined,
startDate as string | undefined,
endDate as string | undefined,
);
res.json(history);
} catch (err) {
next(err);
}
}
22 changes: 12 additions & 10 deletions src/modules/deployments/deployments.routes.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Router } from 'express';
import * as deploymentsController from './deployments.controller';
import { authenticate } from '../../middleware/auth';
import { validateRequest } from '../../middleware/validateRequest';
import { createDeploymentSchema, updateStatusSchema } from './deployments.schema';

const router = Router();

// TODO: Implement deployment management endpoints
// - POST / Create a new deployment
// - GET / List deployments (with filtering by project, environment, status)
// - GET /:id Get deployment details
// - PUT /:id/status Update deployment status (webhook callback)
// - POST /:id/rollback Rollback a deployment
// - GET /stats Get deployment statistics
// Deployment CRUD
router.get('/', authenticate, deploymentsController.listDeployments);
router.get('/history', authenticate, deploymentsController.getHistory);
router.get('/stats/:projectId', authenticate, deploymentsController.getStats);
router.get('/:id', authenticate, deploymentsController.getDeployment);
router.post('/', authenticate, validateRequest(createDeploymentSchema), deploymentsController.createDeployment);
router.put('/:id/status', authenticate, validateRequest(updateStatusSchema), deploymentsController.updateStatus);

router.get('/', authenticate, (_req, res) => {
res.status(501).json({ error: { code: 'NOT_IMPLEMENTED', message: 'Deployment management coming soon' } });
});
// Rollback
router.post('/:id/rollback', authenticate, deploymentsController.rollbackDeployment);

export default router;
24 changes: 24 additions & 0 deletions src/modules/deployments/deployments.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from 'zod';

export const createDeploymentSchema = z.object({
projectId: z.string().uuid(),
environment: z.enum(['DEVELOPMENT', 'STAGING', 'PRODUCTION']),
commitSha: z.string().min(7).max(40).optional(),
changelog: z.string().max(5000).optional(),
});

export const updateStatusSchema = z.object({
status: z.enum(['PENDING', 'IN_PROGRESS', 'SUCCESS', 'FAILED', 'ROLLED_BACK']),
completedAt: z.string().datetime().optional(),
});

export const deploymentHistoryQuerySchema = z.object({
projectId: z.string().uuid().optional(),
environment: z.string().optional(),
status: z.string().optional(),
startDate: z.string().datetime().optional(),
endDate: z.string().datetime().optional(),
});

export type CreateDeploymentInput = z.infer<typeof createDeploymentSchema>;
export type UpdateStatusInput = z.infer<typeof updateStatusSchema>;
Loading