Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

made "Create New Key" always accessible #2414 #2444

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/dashboard/app/(app)/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export default function SettingsLayout({
<div>
<div className="space-y-1 ">
<h1 className="text-2xl font-semibold tracking-tight">Settings</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">Manage your workspace settings.</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
Manage your workspace settings.
</p>
</div>

<Navbar navigation={navigation} className="mt-8" />
Expand Down
266 changes: 168 additions & 98 deletions apps/dashboard/app/(app)/settings/root-keys/new/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ import { VisibleButton } from "@/components/dashboard/visible-button";
import { Button } from "@/components/ui/button";
import { Code } from "@/components/ui/code";

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Dialog,
DialogClose,
Expand All @@ -20,14 +30,21 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "@/components/ui/toaster";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { trpc } from "@/lib/trpc/client";
import { type UnkeyPermission, unkeyPermissionValidation } from "@unkey/rbac";
import { ChevronRight } from "lucide-react";
import { useRouter } from "next/navigation";
import { createParser, parseAsArrayOf, useQueryState } from "nuqs";
import { useEffect, useState } from "react";
import { apiPermissions, workspacePermissions } from "../[keyId]/permissions/permissions";
import {
apiPermissions,
workspacePermissions,
} from "../[keyId]/permissions/permissions";

type Props = {
apis: {
Expand All @@ -54,7 +71,7 @@ export const Client: React.FC<Props> = ({ apis }) => {
history: "push",
shallow: false, // otherwise server components won't notice the change
clearOnDefault: true,
}),
})
);

const key = trpc.rootKey.create.useMutation({
Expand All @@ -64,15 +81,19 @@ export const Client: React.FC<Props> = ({ apis }) => {
},
});

const snippet = `curl -XPOST '${process.env.NEXT_PUBLIC_UNKEY_API_URL ?? "https://api.unkey.dev"}/v1/keys.createKey' \\
const snippet = `curl -XPOST '${
process.env.NEXT_PUBLIC_UNKEY_API_URL ?? "https://api.unkey.dev"
}/v1/keys.createKey' \\
-H 'Authorization: Bearer ${key.data?.key}' \\
-H 'Content-Type: application/json' \\
-d '{
"prefix": "hello",
"apiId": "<API_ID>"
}'`;

const maskedKey = `unkey_${"*".repeat(key.data?.key.split("_").at(1)?.length ?? 0)}`;
const maskedKey = `unkey_${"*".repeat(
key.data?.key.split("_").at(1)?.length ?? 0
)}`;
const [showKey, setShowKey] = useState(false);
const [showKeyInSnippet, setShowKeyInSnippet] = useState(false);

Expand All @@ -85,7 +106,9 @@ export const Client: React.FC<Props> = ({ apis }) => {
});
};

const [cardStatesMap, setCardStatesMap] = useState<Record<string, boolean>>({});
const [cardStatesMap, setCardStatesMap] = useState<Record<string, boolean>>(
{}
);

const toggleCard = (apiId: string) => {
setCardStatesMap((prevStates) => ({
Expand Down Expand Up @@ -117,7 +140,7 @@ export const Client: React.FC<Props> = ({ apis }) => {
}, []); // Execute ones on the first load

return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 pb-20">
<Card>
<CardHeader>
<CardTitle>Name</CardTitle>
Expand All @@ -140,47 +163,81 @@ export const Client: React.FC<Props> = ({ apis }) => {
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4">
{Object.entries(workspacePermissions).map(([category, allPermissions]) => {
const allPermissionNames = Object.values(allPermissions).map(
({ permission }) => permission,
);
const isAllSelected = allPermissionNames.every((permission) =>
selectedPermissions.includes(permission),
);

return (
<div key={`workspace-${category}`} className="flex flex-col gap-2">
<div className="flex flex-col">
<PermissionToggle
permissionName={`selectAll-${category}`}
label={<span className="text-base font-bold">{category}</span>}
description={`Select all permissions for ${category} in this workspace`}
checked={isAllSelected}
setChecked={(isChecked) => {
allPermissionNames.forEach((permission) => {
handleSetChecked(permission, isChecked);
});
}}
/>
</div>
{Object.entries(workspacePermissions).map(
([category, allPermissions]) => {
const allPermissionNames = Object.values(allPermissions).map(
({ permission }) => permission
);
const isAllSelected = allPermissionNames.every((permission) =>
selectedPermissions.includes(permission)
);

<div className="flex flex-col gap-1">
{Object.entries(allPermissions).map(([action, { description, permission }]) => (
return (
<div
key={`workspace-${category}`}
className="flex flex-col gap-2"
>
<div className="flex flex-col">
<PermissionToggle
key={action}
permissionName={permission}
label={action}
description={description}
checked={selectedPermissions.includes(permission)}
setChecked={(isChecked) => handleSetChecked(permission, isChecked)}
permissionName={`selectAll-${category}`}
label={
<span className="text-base font-bold">
{category}
</span>
}
description={`Select all permissions for ${category} in this workspace`}
checked={isAllSelected}
setChecked={(isChecked) => {
allPermissionNames.forEach((permission) => {
handleSetChecked(permission, isChecked);
});
}}
/>
))}
</div>

<div className="flex flex-col gap-1">
{Object.entries(allPermissions).map(
([action, { description, permission }]) => (
<PermissionToggle
key={action}
permissionName={permission}
label={action}
description={description}
checked={selectedPermissions.includes(permission)}
setChecked={(isChecked) =>
handleSetChecked(permission, isChecked)
}
/>
)
)}
</div>
</div>
</div>
);
})}
);
}
)}
</div>
</CardContent>
<div className="sticky bottom-0 bg-background border-t mx-auto">
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
<div className="py-4">
<Button
className="w-full"
onClick={() => {
key.mutate({
name: name && name.length > 0 ? name : undefined,
permissions: selectedPermissions,
});
}}
>
{key.isLoading ? (
<Loading className="w-4 h-4" />
) : (
"Create New Key"
)}
</Button>
</div>
</div>
</div>
</Card>
{apis.map((api) => (
<Collapsible
Expand All @@ -205,71 +262,74 @@ export const Client: React.FC<Props> = ({ apis }) => {
</CollapsibleTrigger>
<CollapsibleContent id={api.id}>
<CardDescription>
Permissions scoped to this API. Enabling these roles only grants access to this
specific API.
Permissions scoped to this API. Enabling these roles only
grants access to this specific API.
</CardDescription>
</CollapsibleContent>
</CardHeader>
<CollapsibleContent>
<CardContent>
<div className="flex flex-col gap-4">
{Object.entries(apiPermissions(api.id)).map(([category, roles]) => {
const allPermissionNames = Object.values(roles).map(
({ permission }) => permission,
);
const isAllSelected = allPermissionNames.every((permission) =>
selectedPermissions.includes(permission),
);

return (
<div key={`api-${category}`} className="flex flex-col gap-2">
<div className="flex flex-col">
<PermissionToggle
permissionName={`selectAll-${category}`}
label={
<span className="text-base font-bold mt-3.5 sm:mt-0">{category}</span>
}
description={`Select all ${category} permissions for this API`}
checked={isAllSelected}
setChecked={(isChecked) => {
allPermissionNames.forEach((permission) => {
handleSetChecked(permission, isChecked);
});
}}
/>
</div>
{Object.entries(apiPermissions(api.id)).map(
([category, roles]) => {
const allPermissionNames = Object.values(roles).map(
({ permission }) => permission
);
const isAllSelected = allPermissionNames.every(
(permission) => selectedPermissions.includes(permission)
);

<div className="flex flex-col gap-1">
{Object.entries(roles).map(([action, { description, permission }]) => (
return (
<div
key={`api-${category}`}
className="flex flex-col gap-2"
>
<div className="flex flex-col">
<PermissionToggle
key={action}
permissionName={permission}
label={action}
description={description}
checked={selectedPermissions.includes(permission)}
setChecked={(isChecked) => handleSetChecked(permission, isChecked)}
permissionName={`selectAll-${category}`}
label={
<span className="text-base font-bold mt-3.5 sm:mt-0">
{category}
</span>
}
description={`Select all ${category} permissions for this API`}
checked={isAllSelected}
setChecked={(isChecked) => {
allPermissionNames.forEach((permission) => {
handleSetChecked(permission, isChecked);
});
}}
/>
))}
</div>

<div className="flex flex-col gap-1">
{Object.entries(roles).map(
([action, { description, permission }]) => (
<PermissionToggle
key={action}
permissionName={permission}
label={action}
description={description}
checked={selectedPermissions.includes(
permission
)}
setChecked={(isChecked) =>
handleSetChecked(permission, isChecked)
}
/>
)
)}
</div>
</div>
</div>
);
})}
);
}
)}
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
))}
<Button
onClick={() => {
key.mutate({
name: name && name.length > 0 ? name : undefined,
permissions: selectedPermissions,
});
}}
>
{key.isLoading ? <Loading className="w-4 h-4" /> : "Create New Key"}
</Button>

<Dialog
open={!!key.data?.key}
Expand All @@ -287,7 +347,8 @@ export const Client: React.FC<Props> = ({ apis }) => {
<DialogHeader>
<DialogTitle>Your API Key</DialogTitle>
<DialogDescription className="w-fit">
This key is only shown once and can not be recovered. Please store it somewhere safe.
This key is only shown once and can not be recovered. Please store
it somewhere safe.
</DialogDescription>

<Code className="flex items-center justify-between gap-4 my-8 ph-no-capture">
Expand All @@ -304,11 +365,16 @@ export const Client: React.FC<Props> = ({ apis }) => {
</p>
<Code className="flex flex-col items-start gap-2 w-full text-xs">
<div className="w-full shrink-0 flex items-center justify-end gap-2">
<VisibleButton isVisible={showKeyInSnippet} setIsVisible={setShowKeyInSnippet} />
<VisibleButton
isVisible={showKeyInSnippet}
setIsVisible={setShowKeyInSnippet}
/>
<CopyButton value={snippet} />
</div>
<div className="text-wrap">
{showKeyInSnippet ? snippet : snippet.replace(key.data?.key ?? "", maskedKey)}
{showKeyInSnippet
? snippet
: snippet.replace(key.data?.key ?? "", maskedKey)}
</div>
</Code>
<DialogClose asChild>
Expand Down Expand Up @@ -354,13 +420,17 @@ const PermissionToggle: React.FC<PermissionToggleProps> = ({
</div>
</TooltipTrigger>
<TooltipContent className="flex items-center gap-2">
<span className="font-mono text-sm font-medium">{permissionName}</span>
<span className="font-mono text-sm font-medium">
{permissionName}
</span>
<CopyButton value={permissionName} />
</TooltipContent>
</Tooltip>
</div>

<p className="w-full md:w-2/3 text-xs text-content-subtle ml-6 md:ml-0">{description}</p>
<p className="w-full md:w-2/3 text-xs text-content-subtle ml-6 md:ml-0">
{description}
</p>
</div>
);
};
Loading
Loading