{errorMessage &&
{errorMessage}
}
-
+
-
+ {/*
*/}
>
diff --git a/apps/web/components/security/TwoFactorAuthSection.tsx b/apps/web/components/security/TwoFactorAuthSection.tsx
index 00fd4adf0a..6cd07d942c 100644
--- a/apps/web/components/security/TwoFactorAuthSection.tsx
+++ b/apps/web/components/security/TwoFactorAuthSection.tsx
@@ -17,21 +17,25 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
return (
<>
-
-
{t("2fa")}
-
- {enabled ? t("enabled") : t("disabled")}
-
+
+
+
+
{t("2fa")}
+
+ {enabled ? t("enabled") : t("disabled")}
+
+
+
{t("add_an_extra_layer_of_security")}
+
+
+
+
-
{t("add_an_extra_layer_of_security")}
-
-
-
{enableModalOpen && (
{
diff --git a/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx b/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx
new file mode 100644
index 0000000000..28232895b4
--- /dev/null
+++ b/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx
@@ -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,
+ } = 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 (
+
+ );
+}
diff --git a/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx b/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx
new file mode 100644
index 0000000000..21d0af0fc2
--- /dev/null
+++ b/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx
@@ -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(null);
+ return (
+ (
+ <>
+
+
+
{t("api_keys")}
+
{t("api_keys_subtitle")}
+
+
+
+
+
+
+ {data.length ? (
+
+ {data.map((item) => (
+ {
+ setEditing(item);
+ setEditModalOpen(true);
+ }}
+ />
+ ))}
+
+ ) : null}
+
+ {/* New webhook dialog */}
+
+ {/* Edit webhook dialog */}
+
+ >
+ )}
+ />
+ );
+}
diff --git a/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx b/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx
new file mode 100644
index 0000000000..808d511f62
--- /dev/null
+++ b/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx
@@ -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 (
+
+
+
+
+
+ {props?.apiKey?.note}
+
+ {isExpired ? Expired : null}
+
+
+
+ {props?.apiKey?.expiresAt.toLocaleDateString()}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/pages/settings/security.tsx b/apps/web/pages/settings/security.tsx
index eeed2750fd..58e8e57ea3 100644
--- a/apps/web/pages/settings/security.tsx
+++ b/apps/web/pages/settings/security.tsx
@@ -1,10 +1,11 @@
import { IdentityProvider } from "@prisma/client";
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 { identityProviderNameMap } from "@lib/auth";
-import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
import SettingsShell from "@components/SettingsShell";
@@ -34,10 +35,11 @@ export default function Security() {
>
) : (
- <>
+
)}
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 158a2145ca..a135231c37 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -708,5 +708,10 @@
"12_hour": "12 hour",
"24_hour": "24 hour",
"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"
}
diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx
index cf420afe60..dc5552fd25 100644
--- a/apps/web/server/routers/viewer.tsx
+++ b/apps/web/server/routers/viewer.tsx
@@ -20,6 +20,7 @@ import {
} from "@lib/saml";
import slugify from "@lib/slugify";
+import { apiKeysRouter } from "@server/routers/viewer/apiKeys";
import { availabilityRouter } from "@server/routers/viewer/availability";
import { eventTypesRouter } from "@server/routers/viewer/eventTypes";
import { TRPCError } from "@trpc/server";
@@ -800,4 +801,5 @@ export const viewerRouter = createRouter()
.merge("eventTypes.", eventTypesRouter)
.merge("availability.", availabilityRouter)
.merge("teams.", viewerTeamsRouter)
- .merge("webhook.", webhookRouter);
+ .merge("webhook.", webhookRouter)
+ .merge("apiKeys.", apiKeysRouter);
diff --git a/apps/web/server/routers/viewer/apiKeys.tsx b/apps/web/server/routers/viewer/apiKeys.tsx
new file mode 100644
index 0000000000..92897200d0
--- /dev/null
+++ b/apps/web/server/routers/viewer/apiKeys.tsx
@@ -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,
+// };
+// }
+// },
+// });
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index f2f83b5279..162977568f 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -366,12 +366,13 @@ model Webhook {
}
model ApiKey {
- id String @id @unique @default(cuid())
- userId Int
- note String?
- createdAt DateTime @default(now())
- expiresAt DateTime @default(dbgenerated("NOW() + interval '30 days'"))
- lastUsedAt DateTime @default(now())
- hashedKey String @unique()
- user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
+ id String @id @unique @default(cuid())
+ userId Int
+ note String?
+ createdAt DateTime @default(now())
+ neverExpires Boolean @default(false)
+ expiresAt DateTime @default(dbgenerated("NOW() + interval '30 days'"))
+ lastUsedAt DateTime @default(now())
+ hashedKey String @unique()
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
}