import Header from "@/src/components/layouts/header";
import { Button } from "@/src/components/ui/button";
import { Card } from "@/src/components/ui/card";
import { CodeView } from "@/src/components/ui/CodeJsonViewer";
import { Input } from "@/src/components/ui/input";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/src/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/src/components/ui/table";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { CreateApiKeyButton } from "@/src/features/public-api/components/CreateApiKeyButton";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { useHasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
import { api } from "@/src/utils/api";
import { DialogDescription } from "@radix-ui/react-dialog";
import { TrashIcon } from "lucide-react";
import { useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/src/components/ui/alert";
import startCase from "lodash/startCase";
import { useLangfuseEnvCode } from "@/src/features/public-api/hooks/useLangfuseEnvCode";
type ApiKeyScope = "project" | "organization";
type ApiKeyEntity = { id: string; note: string | null };
export function ApiKeyList(props: { entityId: string; scope: ApiKeyScope }) {
const { entityId, scope } = props;
const envCode = useLangfuseEnvCode();
if (!entityId) {
throw new Error(
`${scope}Id is required for ApiKeyList with scope ${scope}`,
);
}
const hasProjectAccess = useHasProjectAccess({
projectId: props.entityId,
scope: "apiKeys:CUD",
});
const hasOrganizationAccess = useHasOrganizationAccess({
organizationId: props.entityId,
scope: "organization:CRUD_apiKeys",
});
const hasAccess =
props.scope === "project" ? hasProjectAccess : hasOrganizationAccess;
const projectApiKeysQuery = api.projectApiKeys.byProjectId.useQuery(
{ projectId: entityId },
{ enabled: hasProjectAccess && props.scope === "project" },
);
const organizationApiKeysQuery =
api.organizationApiKeys.byOrganizationId.useQuery(
{ orgId: entityId },
{ enabled: hasOrganizationAccess && props.scope === "organization" },
);
const apiKeysQuery =
props.scope === "project" ? projectApiKeysQuery : organizationApiKeysQuery;
if (!hasAccess) {
return (
Access Denied
You do not have permission to view API keys for this {scope}.
);
}
return (
}
/>
Created
Note
Public Key
Secret Key
{/* Last used */}
{apiKeysQuery.data?.length === 0 ? (
None
) : (
apiKeysQuery.data?.map((apiKey) => (
{apiKey.createdAt.toLocaleDateString()}
{apiKey.displaySecretKey}
{/*
{apiKey.lastUsedAt?.toLocaleDateString() ?? "Never"}
*/}
))
)}
);
}
// show dialog to let user confirm that this is a destructive action
function DeleteApiKeyButton(props: {
entityId: string;
apiKeyId: string;
scope: ApiKeyScope;
}) {
const { entityId, apiKeyId, scope } = props;
const capture = usePostHogClientCapture();
const hasProjectAccess = useHasProjectAccess({
projectId: props.entityId,
scope: "apiKeys:CUD",
});
const hasOrganizationAccess = useHasOrganizationAccess({
organizationId: props.entityId,
scope: "organization:CRUD_apiKeys",
});
const hasAccess =
props.scope === "project" ? hasProjectAccess : hasOrganizationAccess;
const utils = api.useUtils();
const mutDeleteProjectApiKey = api.projectApiKeys.delete.useMutation({
onSuccess: () => utils.projectApiKeys.invalidate(),
});
const mutDeleteOrgApiKey = api.organizationApiKeys.delete.useMutation({
onSuccess: () => utils.organizationApiKeys.invalidate(),
});
const [open, setOpen] = useState(false);
if (!hasAccess) return null;
const handleDelete = () => {
if (scope === "project") {
mutDeleteProjectApiKey
.mutateAsync({
projectId: entityId,
id: apiKeyId,
})
.then(() => {
capture(`${scope}_settings:api_key_delete`);
setOpen(false);
})
.catch((error) => {
console.error(error);
});
} else {
mutDeleteOrgApiKey
.mutateAsync({
orgId: entityId,
id: apiKeyId,
})
.then(() => {
capture(`${scope}_settings:api_key_delete`);
setOpen(false);
})
.catch((error) => {
console.error(error);
});
}
};
return (
);
}
function ApiKeyNote({
apiKey,
entityId,
scope,
}: {
apiKey: ApiKeyEntity;
entityId: string;
scope: ApiKeyScope;
}) {
const utils = api.useUtils();
const hasProjectAccess = useHasProjectAccess({
projectId: entityId,
scope: "apiKeys:CUD",
});
const hasOrganizationAccess = useHasOrganizationAccess({
organizationId: entityId,
scope: "organization:CRUD_apiKeys",
});
const hasEditAccess =
scope === "project" ? hasProjectAccess : hasOrganizationAccess;
const mutUpdateProjectApiKey = api.projectApiKeys.updateNote.useMutation({
onSuccess: () => utils.projectApiKeys.invalidate(),
});
const mutUpdateOrgApiKey = api.organizationApiKeys.updateNote.useMutation({
onSuccess: () => utils.organizationApiKeys.invalidate(),
});
const [note, setNote] = useState(apiKey.note ?? "");
const [isEditing, setIsEditing] = useState(false);
const handleBlur = () => {
setIsEditing(false);
if (note !== apiKey.note) {
if (scope === "project") {
mutUpdateProjectApiKey.mutate({
projectId: entityId,
keyId: apiKey.id,
note,
});
} else {
mutUpdateOrgApiKey.mutate({
orgId: entityId,
keyId: apiKey.id,
note,
});
}
}
};
if (!hasEditAccess) return note ?? "";
if (isEditing) {
return (
setNote(e.target.value)}
onBlur={handleBlur}
autoFocus
className="h-8"
/>
);
}
return (
setIsEditing(true)}
className="-mx-2 cursor-pointer rounded px-2 py-1 hover:bg-secondary/50"
>
{note || "Click to add note"}
);
}