From d30108c274903c9c6a98b7e6a2beca18bce8c916 Mon Sep 17 00:00:00 2001 From: hmacr Date: Thu, 19 Oct 2023 00:38:04 +0530 Subject: [PATCH] feat: implement API key regeneration --- .../environments/RegenerateApiKeyModal.tsx | 107 ++++++++++++++++++ apps/webapp/app/models/api-key.server.ts | 101 +++++++++++++++++ apps/webapp/app/models/organization.server.ts | 26 +---- .../route.tsx | 17 ++- ...ents.$environmentId.regenerate-api-key.tsx | 39 +++++++ 5 files changed, 260 insertions(+), 30 deletions(-) create mode 100644 apps/webapp/app/components/environments/RegenerateApiKeyModal.tsx create mode 100644 apps/webapp/app/models/api-key.server.ts create mode 100644 apps/webapp/app/routes/resources.environments.$environmentId.regenerate-api-key.tsx diff --git a/apps/webapp/app/components/environments/RegenerateApiKeyModal.tsx b/apps/webapp/app/components/environments/RegenerateApiKeyModal.tsx new file mode 100644 index 0000000000..f94e01cfcc --- /dev/null +++ b/apps/webapp/app/components/environments/RegenerateApiKeyModal.tsx @@ -0,0 +1,107 @@ +import { Button } from "../primitives/Buttons"; +import { Dialog, DialogContent, DialogTrigger } from "~/components/primitives/Dialog"; +import { Header1, Header2 } from "../primitives/Headers"; +import { Input } from "../primitives/Input"; +import { NamedIcon } from "../primitives/NamedIcon"; +import { Paragraph } from "../primitives/Paragraph"; +import { Spinner } from "../primitives/Spinner"; +import { useState } from "react"; +import { useFetcher } from "@remix-run/react"; +import { generateTwoRandomWords } from "~/utils/randomWords"; + +type ModalProps = { + id: string; + title: string; +}; + +type ModalContentProps = ModalProps & { + randomWord: string; + closeModal: () => void; +}; + +export function RegenerateApiKeyModal({ id, title }: ModalProps) { + const randomWord = generateTwoRandomWords(); + const [open, setOpen] = useState(false); + return ( + + + + + + setOpen(false)} + /> + + + ); +} + +const RegenerateApiKeyModalContent = ({ id, title, randomWord, closeModal }: ModalContentProps) => { + const [confirmationText, setConfirmationText] = useState(""); + const fetcher = useFetcher(); + const isSubmitting = fetcher.state === "submitting"; + + // form submission completed + if (fetcher.state === "loading") { + closeModal(); + } + + return ( +
+
+ {title} Environment +
+ + Are you sure you want to regenerate the keys
+ for this environment? +
+ + <> + This will temporarily break any live jobs in the + {title} Environment until the new API keys + are set in the relevant environment variables. + + + + + To confirm, please enter the text:{" "} + {randomWord} + + setConfirmationText(e.target.value)} + /> + + +
+ ); +}; diff --git a/apps/webapp/app/models/api-key.server.ts b/apps/webapp/app/models/api-key.server.ts new file mode 100644 index 0000000000..a54fdae9a1 --- /dev/null +++ b/apps/webapp/app/models/api-key.server.ts @@ -0,0 +1,101 @@ +import { RuntimeEnvironmentType, type RuntimeEnvironment } from "@trigger.dev/database"; +import { prisma } from "~/db.server"; +import { customAlphabet } from "nanoid"; + +const apiKeyId = customAlphabet( + "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + 12 +); + +type RegenerateAPIKeyInput = { + userId: string; + environmentId: string; +}; + +export async function regenerateApiKey({ userId, environmentId }: RegenerateAPIKeyInput) { + const environment = await prisma.runtimeEnvironment.findUnique({ + where: { + id: environmentId, + }, + include: { + organization: true, + project: true, + }, + }); + + if (!environment) { + throw new Error("Environment does not exist"); + } + + // check if the user is part of the org + const organization = await prisma.organization.findFirst({ + where: { + id: environment.organization.id, + members: { some: { userId } }, + }, + }); + + if (!organization) { + throw new Error("User does not have permission to regenerate API key"); + } + + // check if it is the user's dev environment + if (environment.type === RuntimeEnvironmentType.DEVELOPMENT) { + if (!environment.orgMemberId) { + throw new Error("User does not have permission to regenerate API key"); + } + + const orgMember = await prisma.orgMember.findFirst({ + where: { + organizationId: organization.id, + userId: userId, + id: environment.orgMemberId, + }, + }); + + if (!orgMember) { + throw new Error("User does not have permission to regenerate API key"); + } + } + + // generate and store new keys + const newApiKey = createApiKeyForEnv(environment.type); + const newPkApiKey = createPkApiKeyForEnv(environment.type); + + const updatedEnviroment = await prisma.runtimeEnvironment.update({ + data: { + apiKey: newApiKey, + pkApiKey: newPkApiKey, + }, + where: { + id: environmentId, + }, + }); + + return updatedEnviroment; +} + +export function createApiKeyForEnv(envType: RuntimeEnvironment["type"]) { + return `tr_${envSlug(envType)}_${apiKeyId(20)}`; +} + +export function createPkApiKeyForEnv(envType: RuntimeEnvironment["type"]) { + return `pk_${envSlug(envType)}_${apiKeyId(20)}`; +} + +export function envSlug(environmentType: RuntimeEnvironment["type"]) { + switch (environmentType) { + case "DEVELOPMENT": { + return "dev"; + } + case "PRODUCTION": { + return "prod"; + } + case "STAGING": { + return "stg"; + } + case "PREVIEW": { + return "prev"; + } + } +} diff --git a/apps/webapp/app/models/organization.server.ts b/apps/webapp/app/models/organization.server.ts index 2daf0791a5..df0475fbe4 100644 --- a/apps/webapp/app/models/organization.server.ts +++ b/apps/webapp/app/models/organization.server.ts @@ -9,6 +9,7 @@ import { customAlphabet } from "nanoid"; import slug from "slug"; import { prisma, PrismaClientOrTransaction } from "~/db.server"; import { createProject } from "./project.server"; +import { createApiKeyForEnv, createPkApiKeyForEnv, envSlug } from "./api-key.server"; export type { Organization }; @@ -157,28 +158,3 @@ export async function createEnvironment( }, }); } - -function createApiKeyForEnv(envType: RuntimeEnvironment["type"]) { - return `tr_${envSlug(envType)}_${apiKeyId(20)}`; -} - -function createPkApiKeyForEnv(envType: RuntimeEnvironment["type"]) { - return `pk_${envSlug(envType)}_${apiKeyId(20)}`; -} - -function envSlug(environmentType: RuntimeEnvironment["type"]) { - switch (environmentType) { - case "DEVELOPMENT": { - return "dev"; - } - case "PRODUCTION": { - return "prod"; - } - case "STAGING": { - return "stg"; - } - case "PREVIEW": { - return "prev"; - } - } -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx index 521fb993a9..0f22d6869c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx @@ -44,6 +44,7 @@ import { requestUrl } from "~/utils/requestUrl.server"; import { RuntimeEnvironmentType } from "../../../../../packages/database/src"; import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet"; import { FirstEndpointSheet } from "./FirstEndpointSheet"; +import { RegenerateApiKeyModal } from "~/components/environments/RegenerateApiKeyModal"; export const loader = async ({ request, params }: LoaderArgs) => { const userId = await requireUserId(request); @@ -148,11 +149,17 @@ export default function Page() {
{environments.map((environment) => ( -
- - Environment - -
+
+
+ + Environment + + +
+