feat: api keys frontend in security page
parent
d66fd6a6c8
commit
c2cd32995d
|
@ -56,11 +56,11 @@ const ChangePasswordSection = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-6">
|
{/* <div className="mt-6">
|
||||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
|
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
|
||||||
</div>
|
</div> */}
|
||||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
|
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
|
||||||
<div className="py-6 lg:pb-8">
|
<div className="py-6 lg:pb-5">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="w-1/2 ltr:mr-2 rtl:ml-2">
|
<div className="w-1/2 ltr:mr-2 rtl:ml-2">
|
||||||
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
|
||||||
|
@ -99,9 +99,11 @@ const ChangePasswordSection = () => {
|
||||||
</div>
|
</div>
|
||||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||||
<div className="flex justify-end py-8">
|
<div className="flex justify-end py-8">
|
||||||
<Button type="submit">{t("save")}</Button>
|
<Button color="secondary" type="submit">
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-4" />
|
{/* <hr className="mt-4" /> */}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -17,6 +17,8 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className="flex flex-row justify-between truncate pt-9 pl-2">
|
||||||
|
<div className="">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("2fa")}</h2>
|
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("2fa")}</h2>
|
||||||
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
|
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
|
||||||
|
@ -24,14 +26,16 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
|
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="self-center">
|
||||||
<Button
|
<Button
|
||||||
className="mt-6"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
|
color="secondary"
|
||||||
onClick={() => (enabled ? setDisableModalOpen(true) : setEnableModalOpen(true))}>
|
onClick={() => (enabled ? setDisableModalOpen(true) : setEnableModalOpen(true))}>
|
||||||
{enabled ? t("disable") : t("enable")} {t("2fa")}
|
{enabled ? t("disable") : t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{enableModalOpen && (
|
{enableModalOpen && (
|
||||||
<EnableTwoFactorModal
|
<EnableTwoFactorModal
|
||||||
onEnable={() => {
|
onEnable={() => {
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { unknown } from "zod";
|
||||||
|
|
||||||
|
import { generateUniqueAPIKey } from "@calcom/ee/lib/api/apiKeys";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
|
import Button from "@calcom/ui/Button";
|
||||||
|
import { DialogFooter } from "@calcom/ui/Dialog";
|
||||||
|
import Switch from "@calcom/ui/Switch";
|
||||||
|
import { FieldsetLegend, Form, InputGroupBox, TextArea, TextField } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import { DatePicker } from "@components/ui/form/DatePicker";
|
||||||
|
|
||||||
|
import { TApiKeys } from "./ApiKeyListItem";
|
||||||
|
|
||||||
|
export default function ApiKeyDialogForm(props: { defaultValues?: TApiKeys; handleClose: () => void }) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const handleNoteChange = (e) => {
|
||||||
|
form.setValue("note", e.target.value);
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
defaultValues = {
|
||||||
|
id: "",
|
||||||
|
note: "" as string | undefined,
|
||||||
|
expiresAt: "" as unknown as Date,
|
||||||
|
neverExpires: true,
|
||||||
|
hashedKey: "",
|
||||||
|
} as Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt">,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [selectedDate, setSelectedDate] = useState(props?.defaultValues?.expiresAt);
|
||||||
|
const [neverExpired, onNeverExpired] = useState(false);
|
||||||
|
|
||||||
|
const handleDateChange = (e) => {
|
||||||
|
setSelectedDate(e);
|
||||||
|
form.setValue("expiresAt", e);
|
||||||
|
};
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
data-testid="ApiKeyDialogForm"
|
||||||
|
form={form}
|
||||||
|
handleSubmit={async (event) => {
|
||||||
|
if (event.id) {
|
||||||
|
await utils.client.mutation("viewer.apiKeys.edit", event);
|
||||||
|
await utils.invalidateQueries(["viewer.apiKeys.list"]);
|
||||||
|
showToast(t("apiKeys_updated_successfully"), "success");
|
||||||
|
} else {
|
||||||
|
await utils.client.mutation("viewer.apiKeys.create", e);
|
||||||
|
await utils.invalidateQueries(["viewer.apiKeys.list"]);
|
||||||
|
showToast(t("apiKeys_created_successfully"), "success");
|
||||||
|
}
|
||||||
|
props.handleClose();
|
||||||
|
}}
|
||||||
|
className="space-y-4">
|
||||||
|
<input type="hidden" {...form.register("id")} />
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={t("personal_note")}
|
||||||
|
{...form.register("note")}
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
onChange={handleNoteChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex justify-between py-2">
|
||||||
|
<span className="text-md text-gray-600">Expire date</span>
|
||||||
|
<Switch label={"Never expire"} onCheckedChange={onNeverExpired} checked={neverExpired} />
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="neverExpires"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Switch
|
||||||
|
label={field.value ? t("never_expire_key_enabled") : t("never_expire_key_disabled")}
|
||||||
|
defaultChecked={field.value}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
form.setValue("neverExpires", isChecked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DatePicker
|
||||||
|
// disabled={neverExpired}
|
||||||
|
minDate={new Date()}
|
||||||
|
// {...form.register("expiresAt")}
|
||||||
|
date={selectedDate as Date}
|
||||||
|
onDatesChange={handleDateChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" color="secondary" onClick={props.handleClose} tabIndex={-1}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { PlusIcon } from "@heroicons/react/outline";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import Button from "@calcom/ui/Button";
|
||||||
|
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
||||||
|
import ApiKeyDialogForm from "@ee/components/apiKeys/ApiKeyDialogForm";
|
||||||
|
import ApiKeyListItem, { TApiKeys } from "@ee/components/apiKeys/ApiKeyListItem";
|
||||||
|
|
||||||
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import { List } from "@components/List";
|
||||||
|
|
||||||
|
export default function ApiKeyListContainer() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const query = trpc.useQuery(["viewer.apiKeys.list"]);
|
||||||
|
|
||||||
|
const [newApiKeyModal, setNewApiKeyModal] = useState(false);
|
||||||
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
|
const [editing, setEditing] = useState<TApiKeys | null>(null);
|
||||||
|
return (
|
||||||
|
<QueryCell
|
||||||
|
query={query}
|
||||||
|
success={({ data }) => (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-row justify-between truncate pl-2 pr-1 ">
|
||||||
|
<div className="mt-9">
|
||||||
|
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
|
||||||
|
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="self-center">
|
||||||
|
<Button
|
||||||
|
StartIcon={PlusIcon}
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => setNewApiKeyModal(true)}
|
||||||
|
data-testid="new_token">
|
||||||
|
{t("generate_new_token")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.length ? (
|
||||||
|
<List className="pb-6">
|
||||||
|
{data.map((item) => (
|
||||||
|
<ApiKeyListItem
|
||||||
|
key={item.id}
|
||||||
|
apiKey={item}
|
||||||
|
onEditApiKey={() => {
|
||||||
|
setEditing(item);
|
||||||
|
setEditModalOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* New webhook dialog */}
|
||||||
|
<Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
|
||||||
|
<DialogContent>
|
||||||
|
<ApiKeyDialogForm handleClose={() => setNewApiKeyModal(false)} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
{/* Edit webhook dialog */}
|
||||||
|
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||||
|
<DialogContent>
|
||||||
|
{editing && (
|
||||||
|
<ApiKeyDialogForm
|
||||||
|
key={editing.id}
|
||||||
|
handleClose={() => setEditModalOpen(false)}
|
||||||
|
defaultValues={editing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
|
||||||
|
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import Button from "@calcom/ui/Button";
|
||||||
|
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
||||||
|
|
||||||
|
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import { ListItem } from "@components/List";
|
||||||
|
import { Tooltip } from "@components/Tooltip";
|
||||||
|
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||||
|
import Badge from "@components/ui/Badge";
|
||||||
|
|
||||||
|
export type TApiKeys = inferQueryOutput<"viewer.apiKeys.list">[number];
|
||||||
|
|
||||||
|
export default function ApiKeyListItem(props: { apiKey: TApiKeys; onEditApiKey: () => void }) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const isExpired = props.apiKey.expiresAt < new Date();
|
||||||
|
const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete", {
|
||||||
|
async onSuccess() {
|
||||||
|
await utils.invalidateQueries(["viewer.apiKeys.list"]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<ListItem className="-mt-px flex w-full p-4">
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<div className="flex max-w-full flex-col truncate">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<span className={classNames("truncate text-sm", isExpired ? "text-gray-500" : "text-gray-900")}>
|
||||||
|
{props?.apiKey?.note}
|
||||||
|
</span>
|
||||||
|
{isExpired ? <Badge variant="default">Expired</Badge> : null}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
"flex flex-col space-x-2 space-y-1 text-xs sm:flex-row sm:space-y-0 sm:rtl:space-x-reverse",
|
||||||
|
isExpired ? "text-red-500" : "text-gray-900"
|
||||||
|
)}>
|
||||||
|
{props?.apiKey?.expiresAt.toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<Tooltip content={t("edit_api_key")}>
|
||||||
|
<Button
|
||||||
|
onClick={() => props.onEditApiKey()}
|
||||||
|
color="minimal"
|
||||||
|
size="icon"
|
||||||
|
StartIcon={PencilAltIcon}
|
||||||
|
className="ml-4 w-full self-center p-2"></Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog>
|
||||||
|
<Tooltip content={t("delete_api_key")}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
color="minimal"
|
||||||
|
size="icon"
|
||||||
|
StartIcon={TrashIcon}
|
||||||
|
className="ml-2 w-full self-center p-2"></Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</Tooltip>
|
||||||
|
<ConfirmationDialogContent
|
||||||
|
variety="danger"
|
||||||
|
title={t("delete_api-key")}
|
||||||
|
confirmBtnText={t("confirm_delete_api_key")}
|
||||||
|
cancelBtnText={t("cancel")}
|
||||||
|
onConfirm={() =>
|
||||||
|
deleteApiKey.mutate({
|
||||||
|
id: props.apiKey.id,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
{t("delete_api_key_confirmation_message")}
|
||||||
|
</ConfirmationDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import ApiKeyListContainer from "@ee/components/apiKeys/ApiKeyListContainer";
|
||||||
import SAMLConfiguration from "@ee/components/saml/Configuration";
|
import SAMLConfiguration from "@ee/components/saml/Configuration";
|
||||||
|
|
||||||
import { identityProviderNameMap } from "@lib/auth";
|
import { identityProviderNameMap } from "@lib/auth";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import SettingsShell from "@components/SettingsShell";
|
import SettingsShell from "@components/SettingsShell";
|
||||||
|
@ -34,10 +35,11 @@ export default function Security() {
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="space-y-2 divide-y">
|
||||||
<ChangePasswordSection />
|
<ChangePasswordSection />
|
||||||
|
<ApiKeyListContainer />
|
||||||
<TwoFactorAuthSection twoFactorEnabled={user?.twoFactorEnabled || false} />
|
<TwoFactorAuthSection twoFactorEnabled={user?.twoFactorEnabled || false} />
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SAMLConfiguration teamsView={false} teamId={null} />
|
<SAMLConfiguration teamsView={false} teamId={null} />
|
||||||
|
|
|
@ -708,5 +708,10 @@
|
||||||
"12_hour": "12 hour",
|
"12_hour": "12 hour",
|
||||||
"24_hour": "24 hour",
|
"24_hour": "24 hour",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page."
|
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
|
||||||
|
"api_keys": "API Keys",
|
||||||
|
"api_keys_subtitle": "Generate API keys to use for accessing your own account.",
|
||||||
|
"generate_new_token": "Generate new token",
|
||||||
|
"personal_note": "Personal Note",
|
||||||
|
"delete_pat": "Delete personal access token"
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from "@lib/saml";
|
} from "@lib/saml";
|
||||||
import slugify from "@lib/slugify";
|
import slugify from "@lib/slugify";
|
||||||
|
|
||||||
|
import { apiKeysRouter } from "@server/routers/viewer/apiKeys";
|
||||||
import { availabilityRouter } from "@server/routers/viewer/availability";
|
import { availabilityRouter } from "@server/routers/viewer/availability";
|
||||||
import { eventTypesRouter } from "@server/routers/viewer/eventTypes";
|
import { eventTypesRouter } from "@server/routers/viewer/eventTypes";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
@ -800,4 +801,5 @@ export const viewerRouter = createRouter()
|
||||||
.merge("eventTypes.", eventTypesRouter)
|
.merge("eventTypes.", eventTypesRouter)
|
||||||
.merge("availability.", availabilityRouter)
|
.merge("availability.", availabilityRouter)
|
||||||
.merge("teams.", viewerTeamsRouter)
|
.merge("teams.", viewerTeamsRouter)
|
||||||
.merge("webhook.", webhookRouter);
|
.merge("webhook.", webhookRouter)
|
||||||
|
.merge("apiKeys.", apiKeysRouter);
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
|
|
||||||
|
// import { WEBHOOK_TRIGGER_EVENTS } from "@lib/apiKeys/constants";
|
||||||
|
// import sendPayload from "@lib/apiKeys/sendPayload";
|
||||||
|
import { createProtectedRouter } from "@server/createRouter";
|
||||||
|
import { getTranslation } from "@server/lib/i18n";
|
||||||
|
|
||||||
|
export const apiKeysRouter = createProtectedRouter()
|
||||||
|
.query("list", {
|
||||||
|
async resolve({ ctx }) {
|
||||||
|
return await ctx.prisma.apiKey.findMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation("create", {
|
||||||
|
input: z.object({
|
||||||
|
note: z.string().optional().nullish(),
|
||||||
|
hashedKey: z.string(),
|
||||||
|
neverExpires: z.boolean(),
|
||||||
|
expiresAt: z.date().optional(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
return await ctx.prisma.apiKey.create({
|
||||||
|
data: {
|
||||||
|
id: v4(),
|
||||||
|
userId: ctx.user.id,
|
||||||
|
...input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation("edit", {
|
||||||
|
input: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
neverExpires: z.boolean(),
|
||||||
|
note: z.string().optional().nullish(),
|
||||||
|
expiresAt: z.date().optional(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const { id, ...data } = input;
|
||||||
|
const apiKey = await ctx.prisma.apiKey.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!apiKey) {
|
||||||
|
// user does not own this apiKey
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await ctx.prisma.apiKey.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation("delete", {
|
||||||
|
input: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
eventTypeId: z.number().optional(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const { id } = input;
|
||||||
|
await ctx.prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
apiKeys: {
|
||||||
|
delete: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// .mutation("testTrigger", {
|
||||||
|
// input: z.object({
|
||||||
|
// url: z.string().url(),
|
||||||
|
// type: z.string(),
|
||||||
|
// payloadTemplate: z.string().optional().nullable(),
|
||||||
|
// }),
|
||||||
|
// async resolve({ input }) {
|
||||||
|
// const { url, type, payloadTemplate } = input;
|
||||||
|
// const translation = await getTranslation("en", "common");
|
||||||
|
// const language = {
|
||||||
|
// locale: "en",
|
||||||
|
// translate: translation,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const data = {
|
||||||
|
// triggerEvent: "PING",
|
||||||
|
// type: "Test",
|
||||||
|
// title: "Test trigger event",
|
||||||
|
// description: "",
|
||||||
|
// startTime: new Date().toISOString(),
|
||||||
|
// endTime: new Date().toISOString(),
|
||||||
|
// attendees: [
|
||||||
|
// {
|
||||||
|
// email: "jdoe@example.com",
|
||||||
|
// name: "John Doe",
|
||||||
|
// timeZone: "Europe/London",
|
||||||
|
// language,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// organizer: {
|
||||||
|
// name: "Cal",
|
||||||
|
// email: "no-reply@cal.com",
|
||||||
|
// timeZone: "Europe/London",
|
||||||
|
// language,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// return await sendPayload(type, new Date().toISOString(), url, data, payloadTemplate);
|
||||||
|
// } catch (_err) {
|
||||||
|
// const error = getErrorFromUnknown(_err);
|
||||||
|
// return {
|
||||||
|
// ok: false,
|
||||||
|
// status: 500,
|
||||||
|
// message: error.message,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// });
|
|
@ -370,6 +370,7 @@ model ApiKey {
|
||||||
userId Int
|
userId Int
|
||||||
note String?
|
note String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
neverExpires Boolean @default(false)
|
||||||
expiresAt DateTime @default(dbgenerated("NOW() + interval '30 days'"))
|
expiresAt DateTime @default(dbgenerated("NOW() + interval '30 days'"))
|
||||||
lastUsedAt DateTime @default(now())
|
lastUsedAt DateTime @default(now())
|
||||||
hashedKey String @unique()
|
hashedKey String @unique()
|
||||||
|
|
Loading…
Reference in New Issue