Backfill api keys

This commit is contained in:
David Corbitt
2023-08-07 13:08:33 -07:00
parent c9f59bfb79
commit 8f49bace53
5 changed files with 167 additions and 1 deletions

View File

@@ -0,0 +1,43 @@
import { HStack, Icon, IconButton, Tooltip, Text } from "@chakra-ui/react";
import { useCallback, useState } from "react";
import { MdContentCopy } from "react-icons/md";
const CopiableCode = ({ code }: { code: string }) => {
const [copied, setCopied] = useState(false);
const copyToClipboard = useCallback(() => {
const onCopy = async () => {
console.log("copied!");
await navigator.clipboard.writeText(code);
setCopied(true);
};
void onCopy();
}, [code]);
return (
<HStack
backgroundColor="blackAlpha.800"
color="white"
borderRadius={4}
padding={3}
w="full"
justifyContent="space-between"
>
<Text fontFamily="inconsolata" fontWeight="bold" letterSpacing={0.5}>
{code}
</Text>
<Tooltip closeOnClick={false} label={copied ? "Copied!" : "Copy to clipboard"}>
<IconButton
aria-label="Copy"
icon={<Icon as={MdContentCopy} boxSize={5} />}
size="xs"
colorScheme="white"
variant="ghost"
onClick={copyToClipboard}
onMouseLeave={() => setCopied(false)}
/>
</Tooltip>
</HStack>
);
};
export default CopiableCode;

View File

@@ -1,4 +1,13 @@
import { Breadcrumb, BreadcrumbItem, Text } from "@chakra-ui/react"; import {
Breadcrumb,
BreadcrumbItem,
Text,
type TextProps,
VStack,
Input,
Button,
Divider,
} from "@chakra-ui/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AppShell from "~/components/nav/AppShell"; import AppShell from "~/components/nav/AppShell";
@@ -6,11 +15,15 @@ import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
import CopiableCode from "~/components/CopiableCode";
export default function Settings() { export default function Settings() {
const utils = api.useContext(); const utils = api.useContext();
const { data: selectedOrg } = useSelectedOrg(); const { data: selectedOrg } = useSelectedOrg();
const apiKey =
selectedOrg?.apiKeys?.length && selectedOrg?.apiKeys[0] ? selectedOrg?.apiKeys[0].apiKey : "";
const updateMutation = api.organizations.update.useMutation(); const updateMutation = api.organizations.update.useMutation();
const [onSaveName] = useHandledAsyncCallback(async () => { const [onSaveName] = useHandledAsyncCallback(async () => {
if (name && name !== selectedOrg?.name && selectedOrg?.id) { if (name && name !== selectedOrg?.name && selectedOrg?.id) {
@@ -39,6 +52,61 @@ export default function Settings() {
</BreadcrumbItem> </BreadcrumbItem>
</Breadcrumb> </Breadcrumb>
</PageHeaderContainer> </PageHeaderContainer>
<VStack px={8} pt={4} alignItems="flex-start" spacing={4}>
<VStack spacing={0} alignItems="flex-start">
<Text fontSize="2xl" fontWeight="bold">
Project Settings
</Text>
<Text fontSize="sm">
Configure your project settings. These settings only apply to {selectedOrg?.name}.
</Text>
</VStack>
<VStack
w="full"
alignItems="flex-start"
borderWidth={1}
borderRadius={4}
borderColor="gray.300"
p={6}
spacing={6}
>
<VStack alignItems="flex-start" w="full">
<Text fontWeight="bold" fontSize="xl">
Display Name
</Text>
<Input
w="full"
maxW={600}
value={name}
onChange={(e) => setName(e.target.value)}
borderColor="gray.300"
/>
<Button
isDisabled={!name || name === selectedOrg?.name}
colorScheme="orange"
borderRadius={4}
mt={2}
_disabled={{
opacity: 0.6,
}}
onClick={onSaveName}
>
Rename Project
</Button>
</VStack>
<Divider backgroundColor="gray.300" />
<VStack alignItems="flex-start">
<Subtitle>Project API Key</Subtitle>
<Text fontSize="sm">
Use your project API key to authenticate your requests when sending data to OpenPipe. You can set this key in your environment variables,
or use it directly in your code.
</Text>
</VStack>
<CopiableCode code={`OPENPIPE_API_KEY=${apiKey}`} />
</VStack>
</VStack>
</AppShell> </AppShell>
); );
} }
const Subtitle = (props: TextProps) => <Text fontWeight="bold" fontSize="xl" {...props} />;

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db"; import { prisma } from "~/server/db";
import { generateApiKey } from "~/server/utils/generateApiKey";
import { requireCanModifyOrganization, requireNothing } from "~/utils/accessControl"; import { requireCanModifyOrganization, requireNothing } from "~/utils/accessControl";
export const organizationsRouter = createTRPCRouter({ export const organizationsRouter = createTRPCRouter({
@@ -41,6 +42,13 @@ export const organizationsRouter = createTRPCRouter({
role: "ADMIN", role: "ADMIN",
}, },
}), }),
prisma.apiKey.create({
data: {
name: "Default API Key",
organizationId: newOrgId,
apiKey: generateApiKey(),
},
}),
]); ]);
organizations.push(newOrg); organizations.push(newOrg);
} }
@@ -53,6 +61,9 @@ export const organizationsRouter = createTRPCRouter({
where: { where: {
id: input.id, id: input.id,
}, },
include: {
apiKeys: true,
}
}); });
}), }),
update: protectedProcedure update: protectedProcedure

View File

@@ -0,0 +1,33 @@
import { type Prisma } from "@prisma/client";
import { prisma } from "~/server/db";
import { generateApiKey } from "~/server/utils/generateApiKey";
console.log("backfilling api keys");
const organizations = await prisma.organization.findMany({
include: {
apiKeys: true,
},
});
console.log(`found ${organizations.length} organizations`);
const apiKeysToCreate: Prisma.ApiKeyCreateManyInput[] = [];
for (const org of organizations) {
if (!org.apiKeys.length) {
apiKeysToCreate.push({
name: "Default API Key",
organizationId: org.id,
apiKey: generateApiKey(),
});
}
}
console.log(`creating ${apiKeysToCreate.length} api keys`);
await prisma.apiKey.createMany({
data: apiKeysToCreate,
});
console.log("done");

View File

@@ -0,0 +1,11 @@
const KEY_LENGTH = 42;
export const generateApiKey = () => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let randomChars = "";
for (let i = 0; i < KEY_LENGTH; i++) {
randomChars += chars.charAt(Math.floor(Math.random() * chars.length));
}
return `opc_${randomChars}`;
};