diff --git a/app/src/components/CopiableCode.tsx b/app/src/components/CopiableCode.tsx
new file mode 100644
index 0000000..8215b78
--- /dev/null
+++ b/app/src/components/CopiableCode.tsx
@@ -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 (
+
+
+ {code}
+
+
+ }
+ size="xs"
+ colorScheme="white"
+ variant="ghost"
+ onClick={copyToClipboard}
+ onMouseLeave={() => setCopied(false)}
+ />
+
+
+ );
+};
+
+export default CopiableCode;
diff --git a/app/src/pages/settings/index.tsx b/app/src/pages/settings/index.tsx
index 8566bc4..8ca407a 100644
--- a/app/src/pages/settings/index.tsx
+++ b/app/src/pages/settings/index.tsx
@@ -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 AppShell from "~/components/nav/AppShell";
@@ -6,11 +15,15 @@ import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import { api } from "~/utils/api";
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
+import CopiableCode from "~/components/CopiableCode";
export default function Settings() {
const utils = api.useContext();
const { data: selectedOrg } = useSelectedOrg();
+ const apiKey =
+ selectedOrg?.apiKeys?.length && selectedOrg?.apiKeys[0] ? selectedOrg?.apiKeys[0].apiKey : "";
+
const updateMutation = api.organizations.update.useMutation();
const [onSaveName] = useHandledAsyncCallback(async () => {
if (name && name !== selectedOrg?.name && selectedOrg?.id) {
@@ -39,6 +52,61 @@ export default function Settings() {
+
+
+
+ Project Settings
+
+
+ Configure your project settings. These settings only apply to {selectedOrg?.name}.
+
+
+
+
+
+ Display Name
+
+ setName(e.target.value)}
+ borderColor="gray.300"
+ />
+
+
+
+
+ Project API Key
+
+ 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.
+
+
+
+
+
);
}
+
+const Subtitle = (props: TextProps) => ;
diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts
index 195532c..1216f54 100644
--- a/app/src/server/api/routers/organizations.router.ts
+++ b/app/src/server/api/routers/organizations.router.ts
@@ -3,6 +3,7 @@ import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
+import { generateApiKey } from "~/server/utils/generateApiKey";
import { requireCanModifyOrganization, requireNothing } from "~/utils/accessControl";
export const organizationsRouter = createTRPCRouter({
@@ -41,6 +42,13 @@ export const organizationsRouter = createTRPCRouter({
role: "ADMIN",
},
}),
+ prisma.apiKey.create({
+ data: {
+ name: "Default API Key",
+ organizationId: newOrgId,
+ apiKey: generateApiKey(),
+ },
+ }),
]);
organizations.push(newOrg);
}
@@ -53,6 +61,9 @@ export const organizationsRouter = createTRPCRouter({
where: {
id: input.id,
},
+ include: {
+ apiKeys: true,
+ }
});
}),
update: protectedProcedure
diff --git a/app/src/server/scripts/backfillApiKeys.ts b/app/src/server/scripts/backfillApiKeys.ts
new file mode 100644
index 0000000..869a21c
--- /dev/null
+++ b/app/src/server/scripts/backfillApiKeys.ts
@@ -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");
\ No newline at end of file
diff --git a/app/src/server/utils/generateApiKey.ts b/app/src/server/utils/generateApiKey.ts
new file mode 100644
index 0000000..5ff451b
--- /dev/null
+++ b/app/src/server/utils/generateApiKey.ts
@@ -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}`;
+};