diff --git a/.server-changes/sanitize-loader-action-leaks.md b/.server-changes/sanitize-loader-action-leaks.md new file mode 100644 index 0000000000..f4cb871f4c --- /dev/null +++ b/.server-changes/sanitize-loader-action-leaks.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Expand API error response sanitization to additional loaders and actions so internal exception messages (Prisma errors, etc.) no longer leak to callers via 5xx response bodies. diff --git a/apps/webapp/app/routes/api.v1.artifacts.ts b/apps/webapp/app/routes/api.v1.artifacts.ts index 82ae3f5375..c74c66a222 100644 --- a/apps/webapp/app/routes/api.v1.artifacts.ts +++ b/apps/webapp/app/routes/api.v1.artifacts.ts @@ -13,79 +13,85 @@ export async function action({ request }: ActionFunctionArgs) { return json({ error: "Method Not Allowed" }, { status: 405 }); } - const authenticationResult = await authenticateRequest(request, { - apiKey: true, - organizationAccessToken: false, - personalAccessToken: false, - }); + try { + const authenticationResult = await authenticateRequest(request, { + apiKey: true, + organizationAccessToken: false, + personalAccessToken: false, + }); - if (!authenticationResult || !authenticationResult.result.ok) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult || !authenticationResult.result.ok) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const [, rawBody] = await tryCatch(request.json()); - const body = CreateArtifactRequestBody.safeParse(rawBody ?? {}); + const [, rawBody] = await tryCatch(request.json()); + const body = CreateArtifactRequestBody.safeParse(rawBody ?? {}); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - const { environment: authenticatedEnv } = authenticationResult.result; + const { environment: authenticatedEnv } = authenticationResult.result; - const service = new ArtifactsService(); - return await service - .createArtifact(body.data.type, authenticatedEnv, body.data.contentLength) - .match( - (result) => { - return json( - { - artifactKey: result.artifactKey, - uploadUrl: result.uploadUrl, - uploadFields: result.uploadFields, - expiresAt: result.expiresAt.toISOString(), - } satisfies CreateArtifactResponseBody, - { status: 201 } - ); - }, - (error) => { - switch (error.type) { - case "artifact_size_exceeds_limit": { - logger.warn("Artifact size exceeds limit", { error }); - const sizeMB = parseFloat((error.contentLength / (1024 * 1024)).toFixed(1)); - const limitMB = parseFloat((error.sizeLimit / (1024 * 1024)).toFixed(1)); + const service = new ArtifactsService(); + return await service + .createArtifact(body.data.type, authenticatedEnv, body.data.contentLength) + .match( + (result) => { + return json( + { + artifactKey: result.artifactKey, + uploadUrl: result.uploadUrl, + uploadFields: result.uploadFields, + expiresAt: result.expiresAt.toISOString(), + } satisfies CreateArtifactResponseBody, + { status: 201 } + ); + }, + (error) => { + switch (error.type) { + case "artifact_size_exceeds_limit": { + logger.warn("Artifact size exceeds limit", { error }); + const sizeMB = parseFloat((error.contentLength / (1024 * 1024)).toFixed(1)); + const limitMB = parseFloat((error.sizeLimit / (1024 * 1024)).toFixed(1)); - let errorMessage; + let errorMessage; - switch (body.data.type) { - case "deployment_context": - errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB. Make sure you are in the correct directory of your Trigger.dev project. Reach out to us if you are seeing this error consistently.`; - break; - default: - body.data.type satisfies never; - errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB`; + switch (body.data.type) { + case "deployment_context": + errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB. Make sure you are in the correct directory of your Trigger.dev project. Reach out to us if you are seeing this error consistently.`; + break; + default: + body.data.type satisfies never; + errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB`; + } + return json( + { + error: errorMessage, + }, + { status: 400 } + ); + } + case "failed_to_create_presigned_post": { + logger.error("Failed to create presigned POST", { error }); + return json({ error: "Failed to generate artifact upload URL" }, { status: 500 }); + } + case "artifacts_bucket_not_configured": { + logger.error("Artifacts bucket not configured", { error }); + return json({ error: "Internal server error" }, { status: 500 }); + } + default: { + error satisfies never; + logger.error("Failed creating artifact", { error }); + return json({ error: "Internal server error" }, { status: 500 }); } - return json( - { - error: errorMessage, - }, - { status: 400 } - ); - } - case "failed_to_create_presigned_post": { - logger.error("Failed to create presigned POST", { error }); - return json({ error: "Failed to generate artifact upload URL" }, { status: 500 }); - } - case "artifacts_bucket_not_configured": { - logger.error("Artifacts bucket not configured", { error }); - return json({ error: "Internal server error" }, { status: 500 }); - } - default: { - error satisfies never; - logger.error("Failed creating artifact", { error }); - return json({ error: "Internal server error" }, { status: 500 }); } } - } - ); + ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to create artifact", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.auth.jwt.claims.ts b/apps/webapp/app/routes/api.v1.auth.jwt.claims.ts index 0091078dbb..b62874d560 100644 --- a/apps/webapp/app/routes/api.v1.auth.jwt.claims.ts +++ b/apps/webapp/app/routes/api.v1.auth.jwt.claims.ts @@ -1,19 +1,26 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; export async function action({ request }: LoaderFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const claims = { - sub: authenticationResult.environment.id, - pub: true, - }; + const claims = { + sub: authenticationResult.environment.id, + pub: true, + }; - return json(claims); + return json(claims); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to read auth jwt claims", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.auth.jwt.ts b/apps/webapp/app/routes/api.v1.auth.jwt.ts index b95b1eb787..c38cdeb14a 100644 --- a/apps/webapp/app/routes/api.v1.auth.jwt.ts +++ b/apps/webapp/app/routes/api.v1.auth.jwt.ts @@ -1,6 +1,7 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import { z } from "zod"; import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3"; @@ -14,36 +15,42 @@ const RequestBodySchema = z.object({ }); export async function action({ request }: LoaderFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + const parsedBody = RequestBodySchema.safeParse(await request.json()); + + if (!parsedBody.success) { + return json( + { error: "Invalid request body", issues: parsedBody.error.issues }, + { status: 400 } + ); + } + + const claims = { + sub: authenticationResult.environment.id, + pub: true, + ...parsedBody.data.claims, + }; + + // Sign with the environment's current canonical key, not the raw header key, + // so JWTs minted with a revoked (grace-window) key still validate — validation + // in jwtAuth.server.ts uses environment.apiKey. + const jwt = await internal_generateJWT({ + secretKey: authenticationResult.environment.apiKey, + payload: claims, + expirationTime: parsedBody.data.expirationTime ?? "1h", + }); + + return json({ token: jwt }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to mint auth jwt", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } - - const parsedBody = RequestBodySchema.safeParse(await request.json()); - - if (!parsedBody.success) { - return json( - { error: "Invalid request body", issues: parsedBody.error.issues }, - { status: 400 } - ); - } - - const claims = { - sub: authenticationResult.environment.id, - pub: true, - ...parsedBody.data.claims, - }; - - // Sign with the environment's current canonical key, not the raw header key, - // so JWTs minted with a revoked (grace-window) key still validate — validation - // in jwtAuth.server.ts uses environment.apiKey. - const jwt = await internal_generateJWT({ - secretKey: authenticationResult.environment.apiKey, - payload: claims, - expirationTime: parsedBody.data.expirationTime ?? "1h", - }); - - return json({ token: jwt }); } diff --git a/apps/webapp/app/routes/api.v1.authorization-code.ts b/apps/webapp/app/routes/api.v1.authorization-code.ts index b924b67500..2e5c1aadf2 100644 --- a/apps/webapp/app/routes/api.v1.authorization-code.ts +++ b/apps/webapp/app/routes/api.v1.authorization-code.ts @@ -32,7 +32,7 @@ export async function action({ request }: ActionFunctionArgs) { error: error.message, }); - return json({ error: error.message }, { status: 400 }); + return json({ error: "Failed to create authorization code" }, { status: 400 }); } return json({ error: "Something went wrong" }, { status: 500 }); diff --git a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts index 1a5889fab1..edb1973669 100644 --- a/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts +++ b/apps/webapp/app/routes/api.v1.batches.$batchParam.results.ts @@ -12,32 +12,38 @@ const ParamsSchema = z.object({ }); export async function loader({ request, params }: LoaderFunctionArgs) { - // Authenticate the request - const authenticationResult = await authenticateApiRequest(request); + try { + // Authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API Key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API Key" }, { status: 401 }); + } - const parsed = ParamsSchema.safeParse(params); + const parsed = ParamsSchema.safeParse(params); - if (!parsed.success) { - return json({ error: "Invalid or missing run ID" }, { status: 400 }); - } + if (!parsed.success) { + return json({ error: "Invalid or missing run ID" }, { status: 400 }); + } - const { batchParam } = parsed.data; + const { batchParam } = parsed.data; - try { - const presenter = new ApiBatchResultsPresenter(); - const result = await presenter.call(batchParam, authenticationResult.environment); + try { + const presenter = new ApiBatchResultsPresenter(); + const result = await presenter.call(batchParam, authenticationResult.environment); - if (!result) { - return json({ error: "Batch not found" }, { status: 404 }); - } + if (!result) { + return json({ error: "Batch not found" }, { status: 404 }); + } - return json(result); + return json(result); + } catch (error) { + logger.error("Failed to load batch results", { error }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); + } } catch (error) { - logger.error("Failed to load batch results", { error }); - return json({ error: "Something went wrong, please try again." }, { status: 500 }); + if (error instanceof Response) throw error; + logger.error("Failed to load batch results (outer)", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts index c22399ef60..26503c14f9 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts @@ -23,57 +23,63 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); - - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - const authenticatedEnv = authenticationResult.environment; + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const { deploymentId } = parsedParams.data; + const authenticatedEnv = authenticationResult.environment; - const rawBody = await request.json(); - const body = CreateBackgroundWorkerRequestBody.safeParse(rawBody); + const { deploymentId } = parsedParams.data; - if (!body.success) { - return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); - } + const rawBody = await request.json(); + const body = CreateBackgroundWorkerRequestBody.safeParse(rawBody); - const service = new CreateDeploymentBackgroundWorkerServiceV4(); + if (!body.success) { + return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); + } - try { - const backgroundWorker = await service.call(authenticatedEnv, deploymentId, body.data); + const service = new CreateDeploymentBackgroundWorkerServiceV4(); + + try { + const backgroundWorker = await service.call(authenticatedEnv, deploymentId, body.data); + + if (!backgroundWorker) { + return json({ error: "Failed to create background worker" }, { status: 500 }); + } + + return json( + { + id: backgroundWorker.friendlyId, + version: backgroundWorker.version, + contentHash: backgroundWorker.contentHash, + }, + { status: 200 } + ); + } catch (e) { + // Customer-facing validation failures (invalid task config, customer cron + // expression, etc.). The handler returns 4xx with the message; system + // handles it gracefully, no alert needed. + if (e instanceof ServiceValidationError) { + logger.warn("Failed to create background worker", { error: e.message }); + return json({ error: e.message }, { status: e.status ?? 400 }); + } + if (e instanceof CreateDeclarativeScheduleError) { + logger.warn("Failed to create background worker", { error: e.message }); + return json({ error: e.message }, { status: 400 }); + } + + logger.error("Failed to create background worker", { error: e }); - if (!backgroundWorker) { return json({ error: "Failed to create background worker" }, { status: 500 }); } - - return json( - { - id: backgroundWorker.friendlyId, - version: backgroundWorker.version, - contentHash: backgroundWorker.contentHash, - }, - { status: 200 } - ); - } catch (e) { - // Customer-facing validation failures (invalid task config, customer cron - // expression, etc.). The handler returns 4xx with the message; system - // handles it gracefully, no alert needed. - if (e instanceof ServiceValidationError) { - logger.warn("Failed to create background worker", { error: e.message }); - return json({ error: e.message }, { status: e.status ?? 400 }); - } - if (e instanceof CreateDeclarativeScheduleError) { - logger.warn("Failed to create background worker", { error: e.message }); - return json({ error: e.message }, { status: 400 }); - } - - logger.error("Failed to create background worker", { error: e }); - - return json({ error: "Failed to create background worker" }, { status: 500 }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to create deployment background worker", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts index dd209d4494..56b93fe17e 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts @@ -20,53 +20,59 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - const authenticationResult = await authenticateRequest(request, { - apiKey: true, - organizationAccessToken: false, - personalAccessToken: false, - }); + try { + const authenticationResult = await authenticateRequest(request, { + apiKey: true, + organizationAccessToken: false, + personalAccessToken: false, + }); - if (!authenticationResult || !authenticationResult.result.ok) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult || !authenticationResult.result.ok) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const { environment: authenticatedEnv } = authenticationResult.result; - const { deploymentId } = parsedParams.data; + const { environment: authenticatedEnv } = authenticationResult.result; + const { deploymentId } = parsedParams.data; - const [, rawBody] = await tryCatch(request.json()); - const body = CancelDeploymentRequestBody.safeParse(rawBody ?? {}); + const [, rawBody] = await tryCatch(request.json()); + const body = CancelDeploymentRequestBody.safeParse(rawBody ?? {}); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - const deploymentService = new DeploymentService(); + const deploymentService = new DeploymentService(); - return await deploymentService - .cancelDeployment(authenticatedEnv, deploymentId, { - canceledReason: body.data.reason, - }) - .match( - () => { - return new Response(null, { status: 204 }); - }, - (error) => { - switch (error.type) { - case "deployment_not_found": - return json({ error: "Deployment not found" }, { status: 404 }); - case "failed_to_delete_deployment_timeout": - return new Response(null, { status: 204 }); // not a critical error, ignore - case "deployment_cannot_be_cancelled": - return json( - { error: "Deployment is already in a final state and cannot be canceled" }, - { status: 409 } - ); - case "other": - default: - error.type satisfies "other"; - return json({ error: "Internal server error" }, { status: 500 }); + return await deploymentService + .cancelDeployment(authenticatedEnv, deploymentId, { + canceledReason: body.data.reason, + }) + .match( + () => { + return new Response(null, { status: 204 }); + }, + (error) => { + switch (error.type) { + case "deployment_not_found": + return json({ error: "Deployment not found" }, { status: 404 }); + case "failed_to_delete_deployment_timeout": + return new Response(null, { status: 204 }); // not a critical error, ignore + case "deployment_cannot_be_cancelled": + return json( + { error: "Deployment is already in a final state and cannot be canceled" }, + { status: 409 } + ); + case "other": + default: + error.type satisfies "other"; + return json({ error: "Internal server error" }, { status: 500 }); + } } - } - ); + ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to cancel deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts index 5edea5636e..43eb45c536 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.fail.ts @@ -21,32 +21,41 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); - - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const authenticatedEnv = authenticationResult.environment; - - const { deploymentId } = parsedParams.data; - - const rawBody = await request.json(); - const body = FailDeploymentRequestBody.safeParse(rawBody); - - if (!body.success) { - return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); + + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + const authenticatedEnv = authenticationResult.environment; + + const { deploymentId } = parsedParams.data; + + const rawBody = await request.json(); + const body = FailDeploymentRequestBody.safeParse(rawBody); + + if (!body.success) { + return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); + } + + const service = new FailDeploymentService(); + await service.call(authenticatedEnv, deploymentId, body.data); + + return json( + { + id: deploymentId, + }, + { status: 200 } + ); + } catch (error) { + if (error instanceof Response) throw error; + if (error instanceof SyntaxError) { + return json({ error: "Invalid JSON body" }, { status: 400 }); + } + logger.error("Failed to fail deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } - - const service = new FailDeploymentService(); - await service.call(authenticatedEnv, deploymentId, body.data); - - return json( - { - id: deploymentId, - }, - { status: 200 } - ); } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts index 9bafd8644a..c1ce30bbe7 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.finalize.ts @@ -22,41 +22,47 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); - - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - const authenticatedEnv = authenticationResult.environment; + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const { deploymentId } = parsedParams.data; + const authenticatedEnv = authenticationResult.environment; - const rawBody = await request.json(); - const body = FinalizeDeploymentRequestBody.safeParse(rawBody); + const { deploymentId } = parsedParams.data; - if (!body.success) { - return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); - } + const rawBody = await request.json(); + const body = FinalizeDeploymentRequestBody.safeParse(rawBody); - try { - const service = new FinalizeDeploymentService(); - await service.call(authenticatedEnv, deploymentId, body.data); - - return json( - { - id: deploymentId, - }, - { status: 200 } - ); - } catch (error) { - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 400 }); + if (!body.success) { + return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); } - logger.error("Error finalizing deployment", { error }); - return json({ error: "Internal server error" }, { status: 500 }); + try { + const service = new FinalizeDeploymentService(); + await service.call(authenticatedEnv, deploymentId, body.data); + + return json( + { + id: deploymentId, + }, + { status: 200 } + ); + } catch (error) { + if (error instanceof ServiceValidationError) { + return json({ error: error.message }, { status: 400 }); + } + + logger.error("Error finalizing deployment", { error }); + return json({ error: "Internal server error" }, { status: 500 }); + } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to finalize deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts index 161f37f930..89aaad4301 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts @@ -24,77 +24,83 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - const authenticationResult = await authenticateRequest(request, { - apiKey: true, - organizationAccessToken: false, - personalAccessToken: false, - }); + try { + const authenticationResult = await authenticateRequest(request, { + apiKey: true, + organizationAccessToken: false, + personalAccessToken: false, + }); - if (!authenticationResult || !authenticationResult.result.ok) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult || !authenticationResult.result.ok) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const { environment: authenticatedEnv } = authenticationResult.result; - const { deploymentId } = parsedParams.data; + const { environment: authenticatedEnv } = authenticationResult.result; + const { deploymentId } = parsedParams.data; - const [, rawBody] = await tryCatch(request.json()); - const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {}); + const [, rawBody] = await tryCatch(request.json()); + const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {}); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - const deploymentService = new DeploymentService(); + const deploymentService = new DeploymentService(); - return await deploymentService.generateRegistryCredentials(authenticatedEnv, deploymentId).match( - (result) => { - return json( - { - username: result.username, - password: result.password, - expiresAt: result.expiresAt.toISOString(), - repositoryUri: result.repositoryUri, - } satisfies GenerateRegistryCredentialsResponseBody, - { status: 200 } - ); - }, - (error) => { - switch (error.type) { - case "deployment_not_found": - return json({ error: "Deployment not found" }, { status: 404 }); - case "deployment_has_no_image_reference": - logger.error( - "Failed to generate registry credentials: deployment_has_no_image_reference", - { deploymentId } - ); - return json({ error: "Deployment has no image reference" }, { status: 409 }); - case "deployment_is_already_final": - return json( - { error: "Failed to generate registry credentials: deployment_is_already_final" }, - { status: 409 } - ); - case "missing_registry_credentials": - logger.error("Failed to generate registry credentials: missing_registry_credentials", { - deploymentId, - }); - return json({ error: "Missing registry credentials" }, { status: 409 }); - case "registry_not_supported": - logger.error("Failed to generate registry credentials: registry_not_supported", { - deploymentId, - }); - return json({ error: "Registry not supported" }, { status: 409 }); - case "registry_region_not_supported": - logger.error("Failed to generate registry credentials: registry_region_not_supported", { - deploymentId, - }); - return json({ error: "Registry region not supported" }, { status: 409 }); - case "other": - default: - error.type satisfies "other"; - logger.error("Failed to generate registry credentials", { error: error.cause }); - return json({ error: "Internal server error" }, { status: 500 }); + return await deploymentService.generateRegistryCredentials(authenticatedEnv, deploymentId).match( + (result) => { + return json( + { + username: result.username, + password: result.password, + expiresAt: result.expiresAt.toISOString(), + repositoryUri: result.repositoryUri, + } satisfies GenerateRegistryCredentialsResponseBody, + { status: 200 } + ); + }, + (error) => { + switch (error.type) { + case "deployment_not_found": + return json({ error: "Deployment not found" }, { status: 404 }); + case "deployment_has_no_image_reference": + logger.error( + "Failed to generate registry credentials: deployment_has_no_image_reference", + { deploymentId } + ); + return json({ error: "Deployment has no image reference" }, { status: 409 }); + case "deployment_is_already_final": + return json( + { error: "Failed to generate registry credentials: deployment_is_already_final" }, + { status: 409 } + ); + case "missing_registry_credentials": + logger.error("Failed to generate registry credentials: missing_registry_credentials", { + deploymentId, + }); + return json({ error: "Missing registry credentials" }, { status: 409 }); + case "registry_not_supported": + logger.error("Failed to generate registry credentials: registry_not_supported", { + deploymentId, + }); + return json({ error: "Registry not supported" }, { status: 409 }); + case "registry_region_not_supported": + logger.error("Failed to generate registry credentials: registry_region_not_supported", { + deploymentId, + }); + return json({ error: "Registry region not supported" }, { status: 409 }); + case "other": + default: + error.type satisfies "other"; + logger.error("Failed to generate registry credentials", { error: error.cause }); + return json({ error: "Internal server error" }, { status: 500 }); + } } - } - ); + ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to generate registry credentials", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts index 2c78c59f55..671f42606a 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts @@ -20,62 +20,68 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - const authenticationResult = await authenticateRequest(request, { - apiKey: true, - organizationAccessToken: false, - personalAccessToken: false, - }); + try { + const authenticationResult = await authenticateRequest(request, { + apiKey: true, + organizationAccessToken: false, + personalAccessToken: false, + }); - if (!authenticationResult || !authenticationResult.result.ok) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult || !authenticationResult.result.ok) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const { environment: authenticatedEnv } = authenticationResult.result; - const { deploymentId } = parsedParams.data; + const { environment: authenticatedEnv } = authenticationResult.result; + const { deploymentId } = parsedParams.data; - const [, rawBody] = await tryCatch(request.json()); - const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {}); + const [, rawBody] = await tryCatch(request.json()); + const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {}); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - const deploymentService = new DeploymentService(); + const deploymentService = new DeploymentService(); - return await deploymentService - .progressDeployment(authenticatedEnv, deploymentId, { - contentHash: body.data.contentHash, - git: body.data.gitMeta, - runtime: body.data.runtime, - buildServerMetadata: body.data.buildServerMetadata, - }) - .match( - () => { - return new Response(null, { status: 204 }); - }, - (error) => { - switch (error.type) { - case "failed_to_extend_deployment_timeout": { - logger.warn("Failed to extend deployment timeout", { error: error.cause }); - return new Response(null, { status: 204 }); // ignore these errors for now + return await deploymentService + .progressDeployment(authenticatedEnv, deploymentId, { + contentHash: body.data.contentHash, + git: body.data.gitMeta, + runtime: body.data.runtime, + buildServerMetadata: body.data.buildServerMetadata, + }) + .match( + () => { + return new Response(null, { status: 204 }); + }, + (error) => { + switch (error.type) { + case "failed_to_extend_deployment_timeout": { + logger.warn("Failed to extend deployment timeout", { error: error.cause }); + return new Response(null, { status: 204 }); // ignore these errors for now + } + case "deployment_not_found": + return json({ error: "Deployment not found" }, { status: 404 }); + case "deployment_cannot_be_progressed": + return json( + { error: "Deployment is not in a progressable state (PENDING or INSTALLING)" }, + { status: 409 } + ); + case "failed_to_create_remote_build": { + logger.error("Failed to create remote Depot build", { error: error.cause }); + return json({ error: "Failed to create remote build" }, { status: 500 }); + } + case "other": + default: + error.type satisfies "other"; + return json({ error: "Internal server error" }, { status: 500 }); } - case "deployment_not_found": - return json({ error: "Deployment not found" }, { status: 404 }); - case "deployment_cannot_be_progressed": - return json( - { error: "Deployment is not in a progressable state (PENDING or INSTALLING)" }, - { status: 409 } - ); - case "failed_to_create_remote_build": { - logger.error("Failed to create remote Depot build", { error: error.cause }); - return json({ error: "Failed to create remote build" }, { status: 500 }); - } - case "other": - default: - error.type satisfies "other"; - return json({ error: "Internal server error" }, { status: 500 }); } - } - ); + ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to progress deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts index d0593e564f..dd16fabf24 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts @@ -16,70 +16,76 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const { deploymentId } = parsedParams.data; + const { deploymentId } = parsedParams.data; - const deployment = await prisma.workerDeployment.findFirst({ - where: { - friendlyId: deploymentId, - environmentId: authenticatedEnv.id, - }, - include: { - worker: { - include: { - tasks: true, + const deployment = await prisma.workerDeployment.findFirst({ + where: { + friendlyId: deploymentId, + environmentId: authenticatedEnv.id, + }, + include: { + worker: { + include: { + tasks: true, + }, }, + integrationDeployments: true, }, - integrationDeployments: true, - }, - }); + }); - if (!deployment) { - return json({ error: "Deployment not found" }, { status: 404 }); - } + if (!deployment) { + return json({ error: "Deployment not found" }, { status: 404 }); + } - return json({ - id: deployment.friendlyId, - status: deployment.status, - contentHash: deployment.contentHash, - shortCode: deployment.shortCode, - version: deployment.version, - imageReference: deployment.imageReference, - imagePlatform: deployment.imagePlatform, - commitSHA: deployment.commitSHA, - externalBuildData: - deployment.externalBuildData as GetDeploymentResponseBody["externalBuildData"], - errorData: deployment.errorData as GetDeploymentResponseBody["errorData"], - worker: deployment.worker - ? { - id: deployment.worker.friendlyId, - version: deployment.worker.version, - tasks: deployment.worker.tasks.map((task) => ({ - id: task.friendlyId, - slug: task.slug, - filePath: task.filePath, - exportName: task.exportName ?? "@deprecated", - })), - } - : undefined, - integrationDeployments: - deployment.integrationDeployments.length > 0 - ? deployment.integrationDeployments.map((id) => ({ - id: id.id, - integrationName: id.integrationName, - integrationDeploymentId: id.integrationDeploymentId, - commitSHA: id.commitSHA, - createdAt: id.createdAt, - })) + return json({ + id: deployment.friendlyId, + status: deployment.status, + contentHash: deployment.contentHash, + shortCode: deployment.shortCode, + version: deployment.version, + imageReference: deployment.imageReference, + imagePlatform: deployment.imagePlatform, + commitSHA: deployment.commitSHA, + externalBuildData: + deployment.externalBuildData as GetDeploymentResponseBody["externalBuildData"], + errorData: deployment.errorData as GetDeploymentResponseBody["errorData"], + worker: deployment.worker + ? { + id: deployment.worker.friendlyId, + version: deployment.worker.version, + tasks: deployment.worker.tasks.map((task) => ({ + id: task.friendlyId, + slug: task.slug, + filePath: task.filePath, + exportName: task.exportName ?? "@deprecated", + })), + } : undefined, - } satisfies GetDeploymentResponseBody); + integrationDeployments: + deployment.integrationDeployments.length > 0 + ? deployment.integrationDeployments.map((id) => ({ + id: id.id, + integrationName: id.integrationName, + integrationDeploymentId: id.integrationDeploymentId, + commitSHA: id.commitSHA, + createdAt: id.createdAt, + })) + : undefined, + } satisfies GetDeploymentResponseBody); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts index 893b260dc8..9f3b4cad18 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentVersion.promote.ts @@ -22,49 +22,55 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const url = new URL(request.url); - const allowRollbacks = url.searchParams.get("allowRollbacks") === "true"; + const url = new URL(request.url); + const allowRollbacks = url.searchParams.get("allowRollbacks") === "true"; - const { deploymentVersion } = parsedParams.data; + const { deploymentVersion } = parsedParams.data; - const deployment = await prisma.workerDeployment.findFirst({ - where: { - version: deploymentVersion, - environmentId: authenticatedEnv.id, - }, - }); + const deployment = await prisma.workerDeployment.findFirst({ + where: { + version: deploymentVersion, + environmentId: authenticatedEnv.id, + }, + }); - if (!deployment) { - return json({ error: "Deployment not found" }, { status: 404 }); - } + if (!deployment) { + return json({ error: "Deployment not found" }, { status: 404 }); + } - try { - const service = new ChangeCurrentDeploymentService(); - await service.call(deployment, "promote", allowRollbacks); + try { + const service = new ChangeCurrentDeploymentService(); + await service.call(deployment, "promote", allowRollbacks); - return json( - { - id: deployment.friendlyId, - version: deployment.version, - shortCode: deployment.shortCode, - }, - { status: 200 } - ); - } catch (error) { - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 400 }); - } else { - return json({ error: "Failed to promote deployment" }, { status: 500 }); + return json( + { + id: deployment.friendlyId, + version: deployment.version, + shortCode: deployment.shortCode, + }, + { status: 200 } + ); + } catch (error) { + if (error instanceof ServiceValidationError) { + return json({ error: error.message }, { status: 400 }); + } else { + return json({ error: "Failed to promote deployment" }, { status: 500 }); + } } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to promote deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.deployments.latest.ts b/apps/webapp/app/routes/api.v1.deployments.latest.ts index 6f31f58fcc..b8dcb66785 100644 --- a/apps/webapp/app/routes/api.v1.deployments.latest.ts +++ b/apps/webapp/app/routes/api.v1.deployments.latest.ts @@ -5,37 +5,43 @@ import { authenticateApiRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; export async function loader({ request }: LoaderFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const deployment = await prisma.workerDeployment.findFirst({ - where: { - type: WorkerInstanceGroupType.UNMANAGED, - environmentId: authenticatedEnv.id, - }, - orderBy: { - createdAt: "desc", - }, - }); + const deployment = await prisma.workerDeployment.findFirst({ + where: { + type: WorkerInstanceGroupType.UNMANAGED, + environmentId: authenticatedEnv.id, + }, + orderBy: { + createdAt: "desc", + }, + }); - if (!deployment) { - return json({ error: "Deployment not found" }, { status: 404 }); - } + if (!deployment) { + return json({ error: "Deployment not found" }, { status: 404 }); + } - return json({ - id: deployment.friendlyId, - status: deployment.status, - contentHash: deployment.contentHash, - shortCode: deployment.shortCode, - version: deployment.version, - imageReference: deployment.imageReference, - errorData: deployment.errorData, - }); + return json({ + id: deployment.friendlyId, + status: deployment.status, + contentHash: deployment.contentHash, + shortCode: deployment.shortCode, + version: deployment.version, + imageReference: deployment.imageReference, + errorData: deployment.errorData, + }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load latest deployment", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts index dc1791dabc..47eb82c693 100644 --- a/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts +++ b/apps/webapp/app/routes/api.v1.orgs.$orgParam.projects.ts @@ -20,17 +20,72 @@ const ParamsSchema = z.object({ export async function loader({ request, params }: LoaderFunctionArgs) { logger.info("get projects", { url: request.url }); - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + try { + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } + + const { orgParam } = ParamsSchema.parse(params); + + const projects = await prisma.project.findMany({ + where: { + organization: { + ...orgParamWhereClause(orgParam), + deletedAt: null, + members: { + some: { + userId: authenticationResult.userId, + }, + }, + }, + version: "V3", + deletedAt: null, + }, + include: { + organization: true, + }, + }); + + if (!projects) { + return json({ error: "Projects not found" }, { status: 404 }); + } + + const result: GetProjectsResponseBody = projects.map((project) => ({ + id: project.id, + externalRef: project.externalRef, + name: project.name, + slug: project.slug, + createdAt: project.createdAt, + organization: { + id: project.organization.id, + title: project.organization.title, + slug: project.organization.slug, + createdAt: project.organization.createdAt, + }, + })); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to list org projects", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } +} - const { orgParam } = ParamsSchema.parse(params); +export async function action({ request, params }: ActionFunctionArgs) { + try { + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - const projects = await prisma.project.findMany({ - where: { - organization: { + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } + + const { orgParam } = ParamsSchema.parse(params); + + const organization = await prisma.organization.findFirst({ + where: { ...orgParamWhereClause(orgParam), deletedAt: null, members: { @@ -39,95 +94,53 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }, }, }, - version: "V3", - deletedAt: null, - }, - include: { - organization: true, - }, - }); - - if (!projects) { - return json({ error: "Projects not found" }, { status: 404 }); - } - - const result: GetProjectsResponseBody = projects.map((project) => ({ - id: project.id, - externalRef: project.externalRef, - name: project.name, - slug: project.slug, - createdAt: project.createdAt, - organization: { - id: project.organization.id, - title: project.organization.title, - slug: project.organization.slug, - createdAt: project.organization.createdAt, - }, - })); - - return json(result); -} - -export async function action({ request, params }: ActionFunctionArgs) { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } - - const { orgParam } = ParamsSchema.parse(params); - - const organization = await prisma.organization.findFirst({ - where: { - ...orgParamWhereClause(orgParam), - deletedAt: null, - members: { - some: { - userId: authenticationResult.userId, - }, + }); + + if (!organization) { + return json({ error: "Organization not found" }, { status: 404 }); + } + + const body = await request.json(); + const parsedBody = CreateProjectRequestBody.safeParse(body); + + if (!parsedBody.success) { + return json({ error: "Invalid request body" }, { status: 400 }); + } + + const [error, project] = await tryCatch( + createProject({ + organizationSlug: organization.slug, + name: parsedBody.data.name, + userId: authenticationResult.userId, + version: "v3", + }) + ); + + if (error) { + logger.error("Failed to create project", { error }); + return json({ error: "Failed to create project" }, { status: 400 }); + } + + const result: GetProjectResponseBody = { + id: project.id, + externalRef: project.externalRef, + name: project.name, + slug: project.slug, + createdAt: project.createdAt, + organization: { + id: project.organization.id, + title: project.organization.title, + slug: project.organization.slug, + createdAt: project.organization.createdAt, }, - }, - }); - - if (!organization) { - return json({ error: "Organization not found" }, { status: 404 }); - } - - const body = await request.json(); - const parsedBody = CreateProjectRequestBody.safeParse(body); - - if (!parsedBody.success) { - return json({ error: "Invalid request body" }, { status: 400 }); - } + }; - const [error, project] = await tryCatch( - createProject({ - organizationSlug: organization.slug, - name: parsedBody.data.name, - userId: authenticationResult.userId, - version: "v3", - }) - ); - - if (error) { - return json({ error: error.message }, { status: 400 }); + return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to create org project", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } - - const result: GetProjectResponseBody = { - id: project.id, - externalRef: project.externalRef, - name: project.name, - slug: project.slug, - createdAt: project.createdAt, - organization: { - id: project.organization.id, - title: project.organization.title, - slug: project.organization.slug, - createdAt: project.organization.createdAt, - }, - }; - - return json(result); } function orgParamWhereClause(orgParam: string) { diff --git a/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts b/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts index aaf5468588..6fc85d69cc 100644 --- a/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts +++ b/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts @@ -29,119 +29,125 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return apiCors(request, json({})); } - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return apiCors( - request, - json({ error: "Invalid or Missing Access Token" }, { status: 401 }) - ); - } - - const parsedParams = ParamsSchema.safeParse(params); - if (!parsedParams.success) { - return apiCors( - request, - json({ error: "Invalid parameters" }, { status: 400 }) - ); - } - - const { organizationSlug, projectParam } = parsedParams.data; - - const result = await fromPromise( - (async () => { - // Find the project, verifying org membership - const project = await prisma.project.findFirst({ - where: { - slug: projectParam, - organization: { - slug: organizationSlug, - members: { - some: { - userId: authenticationResult.userId, + try { + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + + if (!authenticationResult) { + return apiCors( + request, + json({ error: "Invalid or Missing Access Token" }, { status: 401 }) + ); + } + + const parsedParams = ParamsSchema.safeParse(params); + if (!parsedParams.success) { + return apiCors( + request, + json({ error: "Invalid parameters" }, { status: 400 }) + ); + } + + const { organizationSlug, projectParam } = parsedParams.data; + + const result = await fromPromise( + (async () => { + // Find the project, verifying org membership + const project = await prisma.project.findFirst({ + where: { + slug: projectParam, + organization: { + slug: organizationSlug, + members: { + some: { + userId: authenticationResult.userId, + }, }, }, + deletedAt: null, }, - deletedAt: null, - }, - select: { - id: true, - name: true, - slug: true, - organizationId: true, - }, - }); - - if (!project) { - return { type: "not_found" as const }; - } - - // Get Vercel integration for the project - const vercelService = new VercelIntegrationService(); - const integration = await vercelService.getVercelProjectIntegration(project.id); + select: { + id: true, + name: true, + slug: true, + organizationId: true, + }, + }); - return { type: "success" as const, project, integration }; - })(), - (error) => error - ); + if (!project) { + return { type: "not_found" as const }; + } - if (result.isErr()) { - logger.error("Failed to fetch Vercel projects", { - error: result.error, - organizationSlug, - projectParam, - }); + // Get Vercel integration for the project + const vercelService = new VercelIntegrationService(); + const integration = await vercelService.getVercelProjectIntegration(project.id); - return apiCors( - request, - json({ error: "Internal server error" }, { status: 500 }) + return { type: "success" as const, project, integration }; + })(), + (error) => error ); - } - if (result.value.type === "not_found") { - return apiCors( - request, - json({ error: "Project not found" }, { status: 404 }) - ); - } + if (result.isErr()) { + logger.error("Failed to fetch Vercel projects", { + error: result.error, + organizationSlug, + projectParam, + }); - const { project, integration } = result.value; + return apiCors( + request, + json({ error: "Internal server error" }, { status: 500 }) + ); + } + + if (result.value.type === "not_found") { + return apiCors( + request, + json({ error: "Project not found" }, { status: 404 }) + ); + } + + const { project, integration } = result.value; + + if (!integration) { + return apiCors( + request, + json({ + connected: false, + vercelProject: null, + config: null, + syncEnvVarsMapping: null, + }) + ); + } + + const { parsedIntegrationData } = integration; - if (!integration) { return apiCors( request, json({ - connected: false, - vercelProject: null, - config: null, - syncEnvVarsMapping: null, + connected: true, + vercelProject: { + id: parsedIntegrationData.vercelProjectId, + name: parsedIntegrationData.vercelProjectName, + teamId: parsedIntegrationData.vercelTeamId, + }, + config: { + atomicBuilds: parsedIntegrationData.config.atomicBuilds, + pullEnvVarsBeforeBuild: parsedIntegrationData.config.pullEnvVarsBeforeBuild, + vercelStagingEnvironment: parsedIntegrationData.config.vercelStagingEnvironment, + }, + syncEnvVarsMapping: parsedIntegrationData.syncEnvVarsMapping, + triggerProject: { + id: project.id, + name: project.name, + slug: project.slug, + }, }) ); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to fetch Vercel projects", { error }); + return apiCors(request, json({ error: "Internal Server Error" }, { status: 500 })); } - - const { parsedIntegrationData } = integration; - - return apiCors( - request, - json({ - connected: true, - vercelProject: { - id: parsedIntegrationData.vercelProjectId, - name: parsedIntegrationData.vercelProjectName, - teamId: parsedIntegrationData.vercelTeamId, - }, - config: { - atomicBuilds: parsedIntegrationData.config.atomicBuilds, - pullEnvVarsBeforeBuild: parsedIntegrationData.config.pullEnvVarsBeforeBuild, - vercelStagingEnvironment: parsedIntegrationData.config.vercelStagingEnvironment, - }, - syncEnvVarsMapping: parsedIntegrationData.syncEnvVarsMapping, - triggerProject: { - id: project.id, - name: project.name, - slug: project.slug, - }, - }) - ); } diff --git a/apps/webapp/app/routes/api.v1.orgs.ts b/apps/webapp/app/routes/api.v1.orgs.ts index 626162f234..31ef3783f3 100644 --- a/apps/webapp/app/routes/api.v1.orgs.ts +++ b/apps/webapp/app/routes/api.v1.orgs.ts @@ -2,36 +2,43 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { GetOrgsResponseBody } from "@trigger.dev/core/v3"; import { prisma } from "~/db.server"; +import { logger } from "~/services/logger.server"; import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; export async function loader({ request }: LoaderFunctionArgs) { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + try { + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const orgs = await prisma.organization.findMany({ - where: { - deletedAt: null, - members: { - some: { - userId: authenticationResult.userId, + const orgs = await prisma.organization.findMany({ + where: { + deletedAt: null, + members: { + some: { + userId: authenticationResult.userId, + }, }, }, - }, - }); + }); - if (!orgs) { - return json({ error: "Orgs not found" }, { status: 404 }); - } + if (!orgs) { + return json({ error: "Orgs not found" }, { status: 404 }); + } - const result: GetOrgsResponseBody = orgs.map((org) => ({ - id: org.id, - title: org.title, - slug: org.slug, - createdAt: org.createdAt, - })); + const result: GetOrgsResponseBody = orgs.map((org) => ({ + id: org.id, + title: org.title, + slug: org.slug, + createdAt: org.createdAt, + })); - return json(result); + return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to list orgs", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts index e4b48ece05..b9cb61e1f2 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts @@ -5,6 +5,7 @@ import { authenticatedEnvironmentForAuthentication, authenticateRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ projectRef: z.string(), @@ -21,52 +22,58 @@ const RequestBodySchema = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { - const authenticationResult = await authenticateRequest(request, { - personalAccessToken: true, - organizationAccessToken: true, - apiKey: false, - }); + try { + const authenticationResult = await authenticateRequest(request, { + personalAccessToken: true, + organizationAccessToken: true, + apiKey: false, + }); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const parsedParams = ParamsSchema.safeParse(params); + const parsedParams = ParamsSchema.safeParse(params); - if (!parsedParams.success) { - return json({ error: "Invalid Params" }, { status: 400 }); - } + if (!parsedParams.success) { + return json({ error: "Invalid Params" }, { status: 400 }); + } - const { projectRef, env } = parsedParams.data; - const triggerBranch = request.headers.get("x-trigger-branch") ?? undefined; + const { projectRef, env } = parsedParams.data; + const triggerBranch = request.headers.get("x-trigger-branch") ?? undefined; - const runtimeEnv = await authenticatedEnvironmentForAuthentication( - authenticationResult, - projectRef, - env, - triggerBranch - ); + const runtimeEnv = await authenticatedEnvironmentForAuthentication( + authenticationResult, + projectRef, + env, + triggerBranch + ); - const parsedBody = RequestBodySchema.safeParse(await request.json()); + const parsedBody = RequestBodySchema.safeParse(await request.json()); - if (!parsedBody.success) { - return json( - { error: "Invalid request body", issues: parsedBody.error.issues }, - { status: 400 } - ); - } + if (!parsedBody.success) { + return json( + { error: "Invalid request body", issues: parsedBody.error.issues }, + { status: 400 } + ); + } - const claims = { - sub: runtimeEnv.id, - pub: true, - ...parsedBody.data.claims, - }; + const claims = { + sub: runtimeEnv.id, + pub: true, + ...parsedBody.data.claims, + }; - const jwt = await internal_generateJWT({ - secretKey: runtimeEnv.apiKey, - payload: claims, - expirationTime: parsedBody.data.expirationTime ?? "1h", - }); + const jwt = await internal_generateJWT({ + secretKey: runtimeEnv.apiKey, + payload: claims, + expirationTime: parsedBody.data.expirationTime ?? "1h", + }); - return json({ token: jwt }); + return json({ token: jwt }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to generate env JWT", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts index e0349aab55..218cc580dd 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts @@ -7,6 +7,7 @@ import { authenticateRequest, branchNameFromRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ projectRef: z.string(), @@ -24,25 +25,31 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const { projectRef, env } = parsedParams.data; - const authenticationResult = await authenticateRequest(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); + try { + const authenticationResult = await authenticateRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + const environment = await authenticatedEnvironmentForAuthentication( + authenticationResult, + projectRef, + env, + branchNameFromRequest(request) + ); + + const result: GetProjectEnvResponse = { + apiKey: environment.apiKey, + name: environment.project.name, + apiUrl: processEnv.API_ORIGIN ?? processEnv.APP_ORIGIN, + projectId: environment.project.id, + }; + + return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load project env", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } - - const environment = await authenticatedEnvironmentForAuthentication( - authenticationResult, - projectRef, - env, - branchNameFromRequest(request) - ); - - const result: GetProjectEnvResponse = { - apiKey: environment.apiKey, - name: environment.project.name, - apiUrl: processEnv.API_ORIGIN ?? processEnv.APP_ORIGIN, - projectId: environment.project.id, - }; - - return json(result); } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts index ddb398b4c2..07774339dc 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts @@ -9,6 +9,7 @@ import { authenticatedEnvironmentForAuthentication, authenticateRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ projectRef: z.string(), @@ -23,34 +24,37 @@ const HeadersSchema = z.object({ type ParamsSchema = z.infer; export async function loader({ request, params }: LoaderFunctionArgs) { - const authenticationResult = await authenticateRequest(request, { - personalAccessToken: true, - organizationAccessToken: true, - apiKey: false, - }); + try { + const authenticationResult = await authenticateRequest(request, { + personalAccessToken: true, + organizationAccessToken: true, + apiKey: false, + }); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const parsedParams = ParamsSchema.safeParse(params); + const parsedParams = ParamsSchema.safeParse(params); - if (!parsedParams.success) { - return json({ error: "Invalid Params" }, { status: 400 }); - } - const { projectRef, env } = parsedParams.data; + if (!parsedParams.success) { + return json({ error: "Invalid Params" }, { status: 400 }); + } + const { projectRef, env } = parsedParams.data; - const parsedHeaders = HeadersSchema.safeParse(Object.fromEntries(request.headers)); - const triggerBranch = parsedHeaders.success ? parsedHeaders.data["x-trigger-branch"] : undefined; + const parsedHeaders = HeadersSchema.safeParse(Object.fromEntries(request.headers)); + const triggerBranch = parsedHeaders.success + ? parsedHeaders.data["x-trigger-branch"] + : undefined; - const runtimeEnv = await authenticatedEnvironmentForAuthentication( - authenticationResult, - projectRef, - env, - triggerBranch - ); + const runtimeEnv = await authenticatedEnvironmentForAuthentication( + authenticationResult, + projectRef, + env, + triggerBranch + ); - const currentWorker = await findCurrentWorkerFromEnvironment( + const currentWorker = await findCurrentWorkerFromEnvironment( { id: runtimeEnv.id, type: runtimeEnv.type, @@ -109,5 +113,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { urls, }; - return json(response); + return json(response); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load worker by tag", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts index a2f2dcf417..7359707561 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.alertChannels.ts @@ -15,81 +15,87 @@ const ParamsSchema = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + try { + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const parsedParams = ParamsSchema.safeParse(params); + const parsedParams = ParamsSchema.safeParse(params); - if (!parsedParams.success) { - return json({ error: "Invalid Params" }, { status: 400 }); - } - - const { projectRef } = parsedParams.data; + if (!parsedParams.success) { + return json({ error: "Invalid Params" }, { status: 400 }); + } - const rawBody = await request.json(); + const { projectRef } = parsedParams.data; - const body = ApiCreateAlertChannel.safeParse(rawBody); + const rawBody = await request.json(); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + const body = ApiCreateAlertChannel.safeParse(rawBody); - const service = new CreateAlertChannelService(); + if (!body.success) { + return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); + } - try { - if (body.data.channel === "email") { - if (!body.data.channelData.email) { - return json({ error: "Email is required" }, { status: 422 }); + const service = new CreateAlertChannelService(); + + try { + if (body.data.channel === "email") { + if (!body.data.channelData.email) { + return json({ error: "Email is required" }, { status: 422 }); + } + + const alertChannel = await service.call(projectRef, authenticationResult.userId, { + name: body.data.name, + alertTypes: body.data.alertTypes.map((type) => + ApiAlertChannelPresenter.alertTypeFromApi(type) + ), + channel: { + type: "EMAIL", + email: body.data.channelData.email, + }, + deduplicationKey: body.data.deduplicationKey, + environmentTypes: body.data.environmentTypes, + }); + + return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel)); } - const alertChannel = await service.call(projectRef, authenticationResult.userId, { - name: body.data.name, - alertTypes: body.data.alertTypes.map((type) => - ApiAlertChannelPresenter.alertTypeFromApi(type) - ), - channel: { - type: "EMAIL", - email: body.data.channelData.email, - }, - deduplicationKey: body.data.deduplicationKey, - environmentTypes: body.data.environmentTypes, - }); - - return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel)); - } + if (body.data.channel === "webhook") { + if (!body.data.channelData.url) { + return json({ error: "webhook url is required" }, { status: 422 }); + } + + const alertChannel = await service.call(projectRef, authenticationResult.userId, { + name: body.data.name, + alertTypes: body.data.alertTypes.map((type) => + ApiAlertChannelPresenter.alertTypeFromApi(type) + ), + channel: { + type: "WEBHOOK", + url: body.data.channelData.url, + secret: body.data.channelData.secret, + }, + deduplicationKey: body.data.deduplicationKey, + environmentTypes: body.data.environmentTypes, + }); + + return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel)); + } - if (body.data.channel === "webhook") { - if (!body.data.channelData.url) { - return json({ error: "webhook url is required" }, { status: 422 }); + return json({ error: "Invalid channel type" }, { status: 422 }); + } catch (error) { + if (error instanceof ServiceValidationError) { + return json({ error: error.message }, { status: 422 }); } - const alertChannel = await service.call(projectRef, authenticationResult.userId, { - name: body.data.name, - alertTypes: body.data.alertTypes.map((type) => - ApiAlertChannelPresenter.alertTypeFromApi(type) - ), - channel: { - type: "WEBHOOK", - url: body.data.channelData.url, - secret: body.data.channelData.secret, - }, - deduplicationKey: body.data.deduplicationKey, - environmentTypes: body.data.environmentTypes, - }); - - return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel)); + logger.error("Failed to create alert channel", { error }); + return json({ error: "Something went wrong, please try again." }, { status: 500 }); } - - return json({ error: "Invalid channel type" }, { status: 422 }); } catch (error) { - if (error instanceof ServiceValidationError) { - return json({ error: error.message }, { status: 422 }); - } - - logger.error("Failed to create alert channel", { error }); - return json({ error: "Something went wrong, please try again." }, { status: 500 }); + if (error instanceof Response) throw error; + logger.error("Failed to create alert channel (outer)", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts index 6b044c9e83..e09bc510d9 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts @@ -6,6 +6,7 @@ import { authenticatedEnvironmentForAuthentication, branchNameFromRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import zlib from "node:zlib"; const ParamsSchema = z.object({ @@ -21,44 +22,45 @@ export async function loader({ params, request }: LoaderFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - const authenticationResult = await authenticateRequest(request); + try { + const authenticationResult = await authenticateRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const environment = await authenticatedEnvironmentForAuthentication( - authenticationResult, - parsedParams.data.projectRef, - parsedParams.data.envSlug, - branchNameFromRequest(request) - ); + const environment = await authenticatedEnvironmentForAuthentication( + authenticationResult, + parsedParams.data.projectRef, + parsedParams.data.envSlug, + branchNameFromRequest(request) + ); - // Find the background worker and tasks and files - const backgroundWorker = await prisma.backgroundWorker.findFirst({ - where: { - runtimeEnvironmentId: environment.id, - version: parsedParams.data.version, - }, - include: { - tasks: true, - files: { - include: { - tasks: { - select: { - slug: true, + // Find the background worker and tasks and files + const backgroundWorker = await prisma.backgroundWorker.findFirst({ + where: { + runtimeEnvironmentId: environment.id, + version: parsedParams.data.version, + }, + include: { + tasks: true, + files: { + include: { + tasks: { + select: { + slug: true, + }, }, }, }, }, - }, - }); + }); - if (!backgroundWorker) { - return json({ error: "Background worker not found" }, { status: 404 }); - } + if (!backgroundWorker) { + return json({ error: "Background worker not found" }, { status: 404 }); + } - return json({ + return json({ id: backgroundWorker.friendlyId, version: backgroundWorker.version, cliVersion: backgroundWorker.cliVersion, @@ -74,14 +76,19 @@ export async function loader({ params, request }: LoaderFunctionArgs) { retryConfig: task.retryConfig, queueConfig: task.queueConfig, })), - files: backgroundWorker.files.map((file) => ({ - id: file.friendlyId, - filePath: file.filePath, - contentHash: file.contentHash, - contents: decompressContent(file.contents), - tasks: Array.from(new Set(file.tasks.map((task) => task.slug))), - })), - }); + files: backgroundWorker.files.map((file) => ({ + id: file.friendlyId, + filePath: file.filePath, + contentHash: file.contentHash, + contents: decompressContent(file.contents), + tasks: Array.from(new Set(file.tasks.map((task) => task.slug))), + })), + }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load background worker", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } function decompressContent(compressedBuffer: Uint8Array): string { diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts index bc9842f0af..12e5eb24dd 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts @@ -25,53 +25,59 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - logger.info("Invalid or missing api key", { url: request.url }); - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + logger.info("Invalid or missing api key", { url: request.url }); + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const authenticatedEnv = authenticationResult.environment; + const authenticatedEnv = authenticationResult.environment; - const { projectRef } = parsedParams.data; + const { projectRef } = parsedParams.data; - const rawBody = await request.json(); - const body = CreateBackgroundWorkerRequestBody.safeParse(rawBody); + const rawBody = await request.json(); + const body = CreateBackgroundWorkerRequestBody.safeParse(rawBody); - if (!body.success) { - return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); - } + if (!body.success) { + return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); + } - const service = new CreateBackgroundWorkerService(); + const service = new CreateBackgroundWorkerService(); - try { - const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data); + try { + const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data); - return json( - { - id: backgroundWorker.friendlyId, - version: backgroundWorker.version, - contentHash: backgroundWorker.contentHash, - }, - { status: 200 } - ); - } catch (e) { - // Customer-facing validation failures (invalid task config, customer cron - // expression, etc.). The handler returns 4xx with the message; system - // handles it gracefully, no alert needed. - if (e instanceof ServiceValidationError) { - logger.warn("Failed to create background worker", { error: e.message }); - return json({ error: e.message }, { status: 400 }); - } - if (e instanceof CreateDeclarativeScheduleError) { - logger.warn("Failed to create background worker", { error: e.message }); - return json({ error: e.message }, { status: 400 }); - } + return json( + { + id: backgroundWorker.friendlyId, + version: backgroundWorker.version, + contentHash: backgroundWorker.contentHash, + }, + { status: 200 } + ); + } catch (e) { + // Customer-facing validation failures (invalid task config, customer cron + // expression, etc.). The handler returns 4xx with the message; system + // handles it gracefully, no alert needed. + if (e instanceof ServiceValidationError) { + logger.warn("Failed to create background worker", { error: e.message }); + return json({ error: e.message }, { status: 400 }); + } + if (e instanceof CreateDeclarativeScheduleError) { + logger.warn("Failed to create background worker", { error: e.message }); + return json({ error: e.message }, { status: 400 }); + } - logger.error("Failed to create background worker", { error: e }); + logger.error("Failed to create background worker", { error: e }); - return json({ error: "Failed to create background worker" }, { status: 500 }); + return json({ error: "Failed to create background worker" }, { status: 500 }); + } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to create project background worker", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.dev-status.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.dev-status.ts index f5f632a822..7d536042fe 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.dev-status.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.dev-status.ts @@ -5,37 +5,44 @@ import { authenticatedEnvironmentForAuthentication, authenticateRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; const ParamsSchema = z.object({ projectRef: z.string(), }); export async function loader({ request, params }: LoaderFunctionArgs) { - const authenticationResult = await authenticateRequest(request, { - personalAccessToken: true, - organizationAccessToken: true, - apiKey: false, - }); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + try { + const authenticationResult = await authenticateRequest(request, { + personalAccessToken: true, + organizationAccessToken: true, + apiKey: false, + }); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } + + const parsedParams = ParamsSchema.safeParse(params); + + if (!parsedParams.success) { + return json({ error: "Invalid Params" }, { status: 400 }); + } + + const { projectRef } = parsedParams.data; + + const runtimeEnv = await authenticatedEnvironmentForAuthentication( + authenticationResult, + projectRef, + "dev" + ); + + const isConnected = await devPresence.isConnected(runtimeEnv.id); + + return json({ isConnected }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load dev status", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } - - const parsedParams = ParamsSchema.safeParse(params); - - if (!parsedParams.success) { - return json({ error: "Invalid Params" }, { status: 400 }); - } - - const { projectRef } = parsedParams.data; - - const runtimeEnv = await authenticatedEnvironmentForAuthentication( - authenticationResult, - projectRef, - "dev" - ); - - const isConnected = await devPresence.isConnected(runtimeEnv.id); - - return json({ isConnected }); } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.$name.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.$name.ts index e3081b090e..00e155622c 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.$name.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.$name.ts @@ -7,6 +7,7 @@ import { authenticatedEnvironmentForAuthentication, branchNameFromRequest, } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server"; const ParamsSchema = z.object({ @@ -22,73 +23,82 @@ export async function action({ params, request }: ActionFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - const authenticationResult = await authenticateRequest(request); + try { + const authenticationResult = await authenticateRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const environment = await authenticatedEnvironmentForAuthentication( - authenticationResult, - parsedParams.data.projectRef, - parsedParams.data.slug, - branchNameFromRequest(request) - ); - - // Find the environment variable - const variable = await prisma.environmentVariable.findFirst({ - where: { - key: parsedParams.data.name, - projectId: environment.project.id, - }, - }); - - if (!variable) { - return json({ error: "Environment variable not found" }, { status: 404 }); - } - - const repository = new EnvironmentVariablesRepository(); - - switch (request.method.toUpperCase()) { - case "DELETE": { - const result = await repository.deleteValue(environment.project.id, { - id: variable.id, - environmentId: environment.id, - }); + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - if (result.success) { - return json({ success: true }); - } else { - return json({ error: result.error }, { status: 400 }); - } + const environment = await authenticatedEnvironmentForAuthentication( + authenticationResult, + parsedParams.data.projectRef, + parsedParams.data.slug, + branchNameFromRequest(request) + ); + + // Find the environment variable + const variable = await prisma.environmentVariable.findFirst({ + where: { + key: parsedParams.data.name, + projectId: environment.project.id, + }, + }); + + if (!variable) { + return json({ error: "Environment variable not found" }, { status: 404 }); } - case "PUT": - case "POST": { - const jsonBody = await request.json(); - const body = UpdateEnvironmentVariableRequestBody.safeParse(jsonBody); + const repository = new EnvironmentVariablesRepository(); - if (!body.success) { - return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 }); - } + switch (request.method.toUpperCase()) { + case "DELETE": { + const result = await repository.deleteValue(environment.project.id, { + id: variable.id, + environmentId: environment.id, + }); - const result = await repository.edit(environment.project.id, { - values: [ - { - value: body.data.value, - environmentId: environment.id, - }, - ], - id: variable.id, - keepEmptyValues: true, - }); - - if (result.success) { - return json({ success: true }); - } else { - return json({ error: result.error }, { status: 400 }); + if (result.success) { + return json({ success: true }); + } else { + return json({ error: result.error }, { status: 400 }); + } + } + case "PUT": + case "POST": { + const jsonBody = await request.json(); + + const body = UpdateEnvironmentVariableRequestBody.safeParse(jsonBody); + + if (!body.success) { + return json( + { error: "Invalid request body", issues: body.error.issues }, + { status: 400 } + ); + } + + const result = await repository.edit(environment.project.id, { + values: [ + { + value: body.data.value, + environmentId: environment.id, + }, + ], + id: variable.id, + keepEmptyValues: true, + }); + + if (result.success) { + return json({ success: true }); + } else { + return json({ error: result.error }, { status: 400 }); + } } } + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to update environment variable", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); } } @@ -99,48 +109,54 @@ export async function loader({ params, request }: LoaderFunctionArgs) { return json({ error: "Invalid params" }, { status: 400 }); } - const authenticationResult = await authenticateRequest(request); + try { + const authenticationResult = await authenticateRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const environment = await authenticatedEnvironmentForAuthentication( - authenticationResult, - parsedParams.data.projectRef, - parsedParams.data.slug, - branchNameFromRequest(request) - ); - - // Find the environment variable - const variable = await prisma.environmentVariable.findFirst({ - where: { - key: parsedParams.data.name, - projectId: environment.project.id, - }, - }); - - if (!variable) { - return json({ error: "Environment variable not found" }, { status: 404 }); - } + const environment = await authenticatedEnvironmentForAuthentication( + authenticationResult, + parsedParams.data.projectRef, + parsedParams.data.slug, + branchNameFromRequest(request) + ); + + // Find the environment variable + const variable = await prisma.environmentVariable.findFirst({ + where: { + key: parsedParams.data.name, + projectId: environment.project.id, + }, + }); + + if (!variable) { + return json({ error: "Environment variable not found" }, { status: 404 }); + } - const repository = new EnvironmentVariablesRepository(); + const repository = new EnvironmentVariablesRepository(); - const variables = await repository.getEnvironmentWithRedactedSecrets( - environment.project.id, - environment.id, - environment.parentEnvironmentId ?? undefined - ); + const variables = await repository.getEnvironmentWithRedactedSecrets( + environment.project.id, + environment.id, + environment.parentEnvironmentId ?? undefined + ); - const environmentVariable = variables.find((v) => v.key === parsedParams.data.name); + const environmentVariable = variables.find((v) => v.key === parsedParams.data.name); - if (!environmentVariable) { - return json({ error: "Environment variable not found" }, { status: 404 }); - } + if (!environmentVariable) { + return json({ error: "Environment variable not found" }, { status: 404 }); + } - return json({ - name: environmentVariable.key, - value: environmentVariable.value, - isSecret: environmentVariable.isSecret, - }); + return json({ + name: environmentVariable.key, + value: environmentVariable.value, + isSecret: environmentVariable.isSecret, + }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to get environment variable", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.projects.ts b/apps/webapp/app/routes/api.v1.projects.ts index 3a12417dce..372a8108f4 100644 --- a/apps/webapp/app/routes/api.v1.projects.ts +++ b/apps/webapp/app/routes/api.v1.projects.ts @@ -8,47 +8,53 @@ import { authenticateApiRequestWithPersonalAccessToken } from "~/services/person export async function loader({ request }: LoaderFunctionArgs) { logger.info("get projects", { url: request.url }); - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + try { + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); + } - const projects = await prisma.project.findMany({ - where: { - organization: { - deletedAt: null, - members: { - some: { - userId: authenticationResult.userId, + const projects = await prisma.project.findMany({ + where: { + organization: { + deletedAt: null, + members: { + some: { + userId: authenticationResult.userId, + }, }, }, + version: "V3", + deletedAt: null, + }, + include: { + organization: true, }, - version: "V3", - deletedAt: null, - }, - include: { - organization: true, - }, - }); + }); - if (!projects) { - return json({ error: "Projects not found" }, { status: 404 }); - } + if (!projects) { + return json({ error: "Projects not found" }, { status: 404 }); + } - const result: GetProjectsResponseBody = projects.map((project) => ({ - id: project.id, - externalRef: project.externalRef, - name: project.name, - slug: project.slug, - createdAt: project.createdAt, - organization: { - id: project.organization.id, - title: project.organization.title, - slug: project.organization.slug, - createdAt: project.organization.createdAt, - }, - })); + const result: GetProjectsResponseBody = projects.map((project) => ({ + id: project.id, + externalRef: project.externalRef, + name: project.name, + slug: project.slug, + createdAt: project.createdAt, + organization: { + id: project.organization.id, + title: project.organization.title, + slug: project.organization.slug, + createdAt: project.organization.createdAt, + }, + })); - return json(result); + return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to list projects", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/api.v1.whoami.ts b/apps/webapp/app/routes/api.v1.whoami.ts index 0ebb70b449..f0dea9a7a5 100644 --- a/apps/webapp/app/routes/api.v1.whoami.ts +++ b/apps/webapp/app/routes/api.v1.whoami.ts @@ -2,32 +2,39 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; import { prisma } from "~/db.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger.server"; export async function loader({ request }: LoaderFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequest(request); + try { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } - const environmentWithUser = await prisma.runtimeEnvironment.findUnique({ - select: { - orgMember: { - select: { - userId: true, + const environmentWithUser = await prisma.runtimeEnvironment.findUnique({ + select: { + orgMember: { + select: { + userId: true, + }, }, }, - }, - where: { - id: authenticationResult.environment.id, - }, - }); + where: { + id: authenticationResult.environment.id, + }, + }); - const result = { - ...authenticationResult.environment, - userId: environmentWithUser?.orgMember?.userId, - }; + const result = { + ...authenticationResult.environment, + userId: environmentWithUser?.orgMember?.userId, + }; - return json(result); + return json(result); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load whoami", { error }); + return json({ error: "Internal Server Error" }, { status: 500 }); + } } diff --git a/apps/webapp/app/routes/resources.platform-changelogs.tsx b/apps/webapp/app/routes/resources.platform-changelogs.tsx index 8eeabd6b04..ed62de3c1d 100644 --- a/apps/webapp/app/routes/resources.platform-changelogs.tsx +++ b/apps/webapp/app/routes/resources.platform-changelogs.tsx @@ -2,6 +2,7 @@ import { json } from "@remix-run/node"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { useFetcher, type ShouldRevalidateFunction } from "@remix-run/react"; import { useEffect, useRef } from "react"; +import { logger } from "~/services/logger.server"; import { requireUserId } from "~/services/session.server"; import { getRecentChangelogs, verifyOrgMembership } from "~/services/platformNotifications.server"; @@ -12,20 +13,29 @@ export type PlatformChangelogsLoaderData = { }; export async function loader({ request }: LoaderFunctionArgs) { - const userId = await requireUserId(request); - const url = new URL(request.url); - const rawOrganizationId = url.searchParams.get("organizationId") ?? undefined; - const rawProjectId = url.searchParams.get("projectId") ?? undefined; + try { + const userId = await requireUserId(request); + const url = new URL(request.url); + const rawOrganizationId = url.searchParams.get("organizationId") ?? undefined; + const rawProjectId = url.searchParams.get("projectId") ?? undefined; - const { organizationId, projectId } = await verifyOrgMembership({ - userId, - organizationId: rawOrganizationId, - projectId: rawProjectId, - }); + const { organizationId, projectId } = await verifyOrgMembership({ + userId, + organizationId: rawOrganizationId, + projectId: rawProjectId, + }); - const changelogs = await getRecentChangelogs({ userId, organizationId, projectId }); + const changelogs = await getRecentChangelogs({ userId, organizationId, projectId }); - return json({ changelogs }); + return json({ changelogs }); + } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load platform changelogs", { error }); + // Polling widget — degrade silently so a transient DB blip doesn't paint + // the dashboard with errors every 60s. Empty payload keeps the consumer's + // fetcher.data shape stable; the fault is recorded server-side. + return json({ changelogs: [] }); + } } const POLL_INTERVAL_MS = 60_000;