Backfill api keys
This commit is contained in:
43
app/src/components/CopiableCode.tsx
Normal file
43
app/src/components/CopiableCode.tsx
Normal 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;
|
||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
33
app/src/server/scripts/backfillApiKeys.ts
Normal file
33
app/src/server/scripts/backfillApiKeys.ts
Normal 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");
|
||||||
11
app/src/server/utils/generateApiKey.ts
Normal file
11
app/src/server/utils/generateApiKey.ts
Normal 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}`;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user