feat: create and delete api keys
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import ContentEditable from "react-contenteditable";
|
||||
import { HiOutlineEye, HiOutlineEyeSlash } from "react-icons/hi2";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
@@ -14,6 +15,7 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
@@ -27,10 +29,13 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
onKeyDown,
|
||||
iconRight,
|
||||
className,
|
||||
type = "text",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
if (contentEditable) {
|
||||
return (
|
||||
<ContentEditable
|
||||
@@ -56,7 +61,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
type={type === "password" && showPassword ? "text" : type}
|
||||
className={twMerge(
|
||||
"block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 text-sm shadow-sm ring-1 ring-inset ring-light-600 placeholder:text-dark-800 focus:ring-2 focus:ring-inset focus:ring-light-700 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:leading-6",
|
||||
prefix && "rounded-l-none",
|
||||
@@ -64,7 +71,21 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{iconRight && (
|
||||
{type === "password" && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 bg-light-50 pl-1 text-xs text-light-900 dark:bg-dark-200 dark:text-dark-900"
|
||||
tabIndex={-1}
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<HiOutlineEyeSlash className="h-4 w-4" />
|
||||
) : (
|
||||
<HiOutlineEye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{iconRight && type !== "password" && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
{iconRight}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import cors from "nextjs-cors";
|
||||
import { createOpenApiNextHandler } from "trpc-to-openapi";
|
||||
|
||||
@@ -16,12 +16,11 @@ export default async function handler(
|
||||
const openApiHandler = createOpenApiNextHandler({
|
||||
router: appRouter,
|
||||
createContext: createRESTContext,
|
||||
responseMeta: () => ({ headers: {} }),
|
||||
onError:
|
||||
env.NODE_ENV === "development"
|
||||
? ({ path, error }) => {
|
||||
console.error(
|
||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||
`❌ REST failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
|
||||
60
apps/web/src/views/settings/components/CreateAPIKeyForm.tsx
Normal file
60
apps/web/src/views/settings/components/CreateAPIKeyForm.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { authClient } from "@kan/auth";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Input from "~/components/Input";
|
||||
|
||||
const CreateAPIKeyForm = ({
|
||||
apiKey,
|
||||
refetchUser,
|
||||
}: {
|
||||
apiKey:
|
||||
| {
|
||||
id: number;
|
||||
prefix: string | null;
|
||||
key: string;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
refetchUser: () => void;
|
||||
}) => {
|
||||
console.log({ apiKey });
|
||||
const handleCreateAPIKey = async () => {
|
||||
await authClient.apiKey.create();
|
||||
|
||||
refetchUser();
|
||||
};
|
||||
|
||||
const handleRevokeAPIKey = async () => {
|
||||
if (!apiKey) return;
|
||||
await authClient.apiKey.delete({
|
||||
keyId: apiKey.id.toString(),
|
||||
});
|
||||
|
||||
refetchUser();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{apiKey ? (
|
||||
<div className="flex gap-2">
|
||||
<div className="mb-4 flex w-full max-w-[325px] items-center gap-2">
|
||||
<Input
|
||||
value={`${apiKey.prefix}${apiKey.key}`}
|
||||
readOnly
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="danger" onClick={handleRevokeAPIKey}>
|
||||
Revoke
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button onClick={handleCreateAPIKey}>Create new key</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateAPIKeyForm;
|
||||
@@ -8,6 +8,7 @@ import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
import Avatar from "./components/Avatar";
|
||||
import CreateAPIKeyForm from "./components/CreateAPIKeyForm";
|
||||
import { CustomURLConfirmation } from "./components/CustomURLConfirmation";
|
||||
import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation";
|
||||
import UpdateDisplayNameForm from "./components/UpdateDisplayNameForm";
|
||||
@@ -18,9 +19,12 @@ import UpdateWorkspaceUrlForm from "./components/UpdateWorkspaceUrlForm";
|
||||
export default function SettingsPage() {
|
||||
const { modalContentType, openModal } = useModal();
|
||||
const { workspace } = useWorkspace();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { data } = api.user.getUser.useQuery();
|
||||
|
||||
const refetchUser = () => utils.user.getUser.refetch();
|
||||
|
||||
const handleOpenBillingPortal = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/stripe/create_billing_session", {
|
||||
@@ -91,7 +95,7 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{process.env.NEXT_PUBLIC_KAN_ENV !== "cloud" && (
|
||||
{process.env.NEXT_PUBLIC_KAN_ENV === "cloud" && (
|
||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
|
||||
Billing
|
||||
@@ -109,6 +113,19 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
|
||||
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
|
||||
API keys
|
||||
</h2>
|
||||
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
|
||||
View and manage your API keys.
|
||||
</p>
|
||||
<CreateAPIKeyForm
|
||||
apiKey={data?.apiKey}
|
||||
refetchUser={refetchUser}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-light-300 dark:border-dark-300">
|
||||
<h2 className="mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
|
||||
Delete workspace
|
||||
|
||||
Reference in New Issue
Block a user