import { z } from "zod"; import { environmentFullTitle } from "~/components/environments/EnvironmentLabel"; import { $replica } from "~/db.server"; import { regenerateApiKey } from "~/models/api-key.server"; import { jsonWithErrorMessage, jsonWithSuccessMessage } from "~/models/message.server"; import { VercelIntegrationRepository } from "~/models/vercelIntegration.server"; import { logger } from "~/services/logger.server"; import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder"; const ParamsSchema = z.object({ environmentId: z.string(), }); export const action = dashboardAction( { params: ParamsSchema, context: async (params) => { const environment = await $replica.runtimeEnvironment.findFirst({ where: { id: params.environmentId }, select: { organizationId: true }, }); return environment ? { organizationId: environment.organizationId } : {}; }, // Env-tier write:apiKeys is enforced in the handler — the target // environment's tier isn't known until we resolve it from the id. }, async ({ request, params, user, ability }) => { if (request.method.toUpperCase() !== "POST") { throw new Response("Method Not Allowed", { status: 405 }); } const { environmentId } = params; const environment = await $replica.runtimeEnvironment.findFirst({ where: { id: environmentId }, select: { type: true }, }); if (!environment) { return jsonWithErrorMessage({ ok: false }, request, "Environment not found"); } // Gate the regenerate even on a direct POST: a role that can't write // this tier's API keys can't rotate them. The disabled UI control is // not the boundary; this check is. if (!ability.can("write", { type: "apiKeys", envType: environment.type })) { return jsonWithErrorMessage( { ok: false }, request, "You don't have permission to regenerate API keys for this environment." ); } const formData = await request.formData(); const syncToVercel = formData.get("syncToVercel") === "on"; try { const updatedEnvironment = await regenerateApiKey({ userId: user.id, environmentId }); // Sync the regenerated API key to Vercel only when requested and not for DEVELOPMENT if (syncToVercel && updatedEnvironment.type !== "DEVELOPMENT") { await syncApiKeyToVercel( updatedEnvironment.projectId, updatedEnvironment.type as "PRODUCTION" | "STAGING" | "PREVIEW", updatedEnvironment.apiKey ); } return jsonWithSuccessMessage( { ok: true }, request, `API keys regenerated for ${environmentFullTitle(updatedEnvironment)} environment` ); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return jsonWithErrorMessage( { ok: false }, request, `API keys could not be regenerated: ${message}` ); } } ); /** * Sync the API key to Vercel. * Errors are logged but won't fail the API key regeneration. */ async function syncApiKeyToVercel( projectId: string, environmentType: "PRODUCTION" | "STAGING" | "PREVIEW" | "DEVELOPMENT", apiKey: string ): Promise { const result = await VercelIntegrationRepository.syncSingleApiKeyToVercel({ projectId, environmentType, apiKey, }); if (result.isErr()) { logger.warn("syncSingleApiKeyToVercel returned failure", { projectId, environmentType, error: result.error.message, }); } }