+
+
{t("change_password")}
+
{errorMessage &&
{errorMessage}
}
-
+
-
>
diff --git a/apps/web/components/security/TwoFactorAuthSection.tsx b/apps/web/components/security/TwoFactorAuthSection.tsx
index 00fd4adf..72aaeb88 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/components/ui/form/DatePicker.tsx b/apps/web/components/ui/form/DatePicker.tsx
index f45d97bd..6edb71df 100644
--- a/apps/web/components/ui/form/DatePicker.tsx
+++ b/apps/web/components/ui/form/DatePicker.tsx
@@ -10,9 +10,11 @@ type Props = {
date: Date;
onDatesChange?: ((date: Date) => void) | undefined;
className?: string;
+ disabled?: boolean;
+ minDate?: Date;
};
-export const DatePicker = ({ date, onDatesChange, className }: Props) => {
+export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }: Props) => {
return (
{
clearIcon={null}
calendarIcon={}
value={date}
+ minDate={minDate}
+ disabled={disabled}
onChange={onDatesChange}
/>
);
diff --git a/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx b/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx
new file mode 100644
index 00000000..b3b168bf
--- /dev/null
+++ b/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx
@@ -0,0 +1,150 @@
+import { ClipboardCopyIcon } from "@heroicons/react/solid";
+import dayjs from "dayjs";
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+
+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 { Form, TextField } from "@calcom/ui/form/fields";
+
+import { trpc } from "@lib/trpc";
+
+import { Tooltip } from "@components/Tooltip";
+import { DatePicker } from "@components/ui/form/DatePicker";
+
+import { TApiKeys } from "./ApiKeyListItem";
+
+export default function ApiKeyDialogForm(props: {
+ title: string;
+ defaultValues?: Omit & { neverExpires: boolean };
+ handleClose: () => void;
+}) {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+
+ const {
+ defaultValues = {
+ note: "",
+ neverExpires: false,
+ expiresAt: dayjs().add(1, "month").toDate(),
+ },
+ } = props;
+
+ const [apiKey, setApiKey] = useState("");
+ const [successfulNewApiKeyModal, setSuccessfulNewApiKeyModal] = useState(false);
+ const [apiKeyDetails, setApiKeyDetails] = useState({
+ id: "",
+ hashedKey: "",
+ expiresAt: null as Date | null,
+ note: "" as string | null,
+ neverExpires: false,
+ });
+
+ const form = useForm({
+ defaultValues,
+ });
+ const watchNeverExpires = form.watch("neverExpires");
+
+ return (
+ <>
+ {successfulNewApiKeyModal ? (
+ <>
+
+
+ {apiKeyDetails ? t("success_api_key_edited") : t("success_api_key_created")}
+
+
+ {t("success_api_key_created_bold_tagline")}{" "}
+ {t("you_will_only_view_it_once")}
+
+
+
+
+
+ {apiKey}
+
+
+
+
+
+
+ {apiKeyDetails.neverExpires
+ ? t("never_expire_key")
+ : `${t("expires")} ${apiKeyDetails?.expiresAt?.toLocaleDateString()}`}
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+ >
+ );
+}
diff --git a/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx b/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx
new file mode 100644
index 00000000..2fee9c2f
--- /dev/null
+++ b/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx
@@ -0,0 +1,77 @@
+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 [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires: boolean }) | null>(null);
+ return (
+ (
+ <>
+
+
+
{t("api_keys")}
+
{t("api_keys_subtitle")}
+
+
+
+
+
+
+ {data.length && (
+
+ {data.map((item: any) => (
+ {
+ setApiKeyToEdit(item);
+ setEditModalOpen(true);
+ }}
+ />
+ ))}
+
+ )}
+
+ {/* New api key dialog */}
+
+ {/* Edit api key dialog */}
+
+ >
+ )}
+ />
+ );
+}
diff --git a/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx b/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx
new file mode 100644
index 00000000..6c74f9fd
--- /dev/null
+++ b/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx
@@ -0,0 +1,107 @@
+import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
+import { ExclamationIcon } from "@heroicons/react/solid";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+
+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";
+
+dayjs.extend(relativeTime);
+
+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 ? props.apiKey.expiresAt < new Date() : null;
+ const neverExpires = props?.apiKey?.expiresAt === null;
+ const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete", {
+ async onSuccess() {
+ await utils.invalidateQueries(["viewer.apiKeys.list"]);
+ },
+ });
+ return (
+
+
+
+
+
+ {props?.apiKey?.note ? props.apiKey.note : t("api_key_no_note")}
+
+ {!neverExpires && isExpired && (
+
+ {t("expired")}
+
+ )}
+
+
+
+ {neverExpires ? (
+
+
+ {t("api_key_never_expires")}
+
+ ) : (
+ `${isExpired ? t("expired") : t("expires")} ${dayjs(
+ props?.apiKey?.expiresAt?.toString()
+ ).fromNow()}`
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/pages/settings/security.tsx b/apps/web/pages/settings/security.tsx
index eeed2750..58e8e57e 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 ab12bd5c..5254f363 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -727,6 +727,33 @@
"redirect_url_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
"duplicate": "Duplicate",
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
+ "api_keys": "API Keys",
+ "api_key_modal_subtitle": "API keys allow you to make API calls for your own account.",
+ "api_keys_subtitle": "Generate API keys to use for accessing your own account.",
+ "generate_new_api_key": "Generate new API key",
+ "create_api_key": "Create an API key",
+ "personal_note": "Name this key",
+ "personal_note_placeholder": "E.g. Development",
+ "api_key_no_note": "Nameless API key",
+ "api_key_never_expires":"This API key has no expiration date",
+ "edit_api_key": "Edit API key",
+ "never_expire_key": "Never expires",
+ "delete_api_key": "Revoke API key",
+ "success_api_key_created": "API key created successfully",
+ "success_api_key_edited": "API key updated successfully",
+ "create": "Create",
+ "success_api_key_created_bold_tagline": "Save this API key somewhere safe.",
+ "you_will_only_view_it_once": "You will not be able to view it again once you close this modal.",
+ "copy_to_clipboard": "Copy to clipboard",
+ "confirm_delete_api_key": "Revoke this API key",
+ "revoke_api_key": "Revoke API key",
+ "api_key_copied": "API key copied!",
+ "delete_api_key_confirm_title": "Permanently remove this API key from your account?",
+ "copy": "Copy",
+ "expire_date": "Expiration date",
+ "expired": "Expired",
+ "never_expires": "Never expires",
+ "expires": "Expires",
"request_reschedule_booking": "Request to reschedule your booking",
"reason_for_reschedule": "Reason for reschedule",
"book_a_new_time": "Book a new time",
diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx
index 36375d46..30dd6f40 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";
@@ -851,4 +852,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 00000000..0b95986f
--- /dev/null
+++ b/apps/web/server/routers/viewer/apiKeys.tsx
@@ -0,0 +1,102 @@
+import { v4 } from "uuid";
+import { z } from "zod";
+
+import { generateUniqueAPIKey } from "@calcom/ee/lib/api/apiKeys";
+
+import { createProtectedRouter } from "@server/createRouter";
+
+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(),
+ expiresAt: z.date().optional().nullable(),
+ neverExpires: z.boolean().optional(),
+ }),
+ async resolve({ ctx, input }) {
+ const [hashedApiKey, apiKey] = generateUniqueAPIKey();
+ // Here we snap never expires before deleting it so it's not passed to prisma create call.
+ const neverExpires = input.neverExpires;
+ delete input.neverExpires;
+ await ctx.prisma.apiKey.create({
+ data: {
+ id: v4(),
+ userId: ctx.user.id,
+ ...input,
+ // And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input
+ expiresAt: neverExpires ? null : input.expiresAt,
+ hashedKey: hashedApiKey,
+ },
+ });
+ const prefixedApiKey = `${process.env.API_KEY_PREFIX ?? "cal_"}${apiKey}`;
+ return prefixedApiKey;
+ },
+ })
+ .mutation("edit", {
+ input: z.object({
+ id: z.string(),
+ note: z.string().optional().nullish(),
+ expiresAt: z.date().optional(),
+ }),
+ async resolve({ ctx, input }) {
+ const { id, ...data } = input;
+ const {
+ apiKeys: [updatedApiKey],
+ } = await ctx.prisma.user.update({
+ where: {
+ id: ctx.user.id,
+ },
+ data: {
+ apiKeys: {
+ update: {
+ where: {
+ id,
+ },
+ data,
+ },
+ },
+ },
+ select: {
+ apiKeys: {
+ where: {
+ id,
+ },
+ },
+ },
+ });
+ return updatedApiKey;
+ },
+ })
+ .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,
+ };
+ },
+ });
diff --git a/packages/ee/lib/api/apiKeys.ts b/packages/ee/lib/api/apiKeys.ts
new file mode 100644
index 00000000..ec0b4f10
--- /dev/null
+++ b/packages/ee/lib/api/apiKeys.ts
@@ -0,0 +1,10 @@
+import { randomBytes, createHash } from "crypto";
+
+// Hash the API key to check against when veriying it. so we don't have to store the key in plain text.
+export const hashAPIKey = (apiKey: string): string => createHash("sha256").update(apiKey).digest("hex");
+
+// Generate a random API key. Prisma already makes sure it's unique. So no need to add salts like with passwords.
+export const generateUniqueAPIKey = (apiKey = randomBytes(16).toString("hex")) => [
+ hashAPIKey(apiKey),
+ apiKey,
+];
diff --git a/packages/prisma/json-schema/json-schema.json b/packages/prisma/json-schema/json-schema.json
new file mode 100644
index 00000000..1ae113fd
--- /dev/null
+++ b/packages/prisma/json-schema/json-schema.json
@@ -0,0 +1,1064 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "EventType": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string",
+ "description": "@zod.nonempty()"
+ },
+ "slug": {
+ "type": "string",
+ "description": "@zod.custom(imports.eventTypeSlug)"
+ },
+ "description": {
+ "type": ["string", "null"]
+ },
+ "position": {
+ "type": "integer",
+ "default": 0
+ },
+ "locations": {
+ "type": ["number", "string", "boolean", "object", "array", "null"],
+ "description": "@zod.custom(imports.eventTypeLocations)"
+ },
+ "length": {
+ "type": "integer"
+ },
+ "hidden": {
+ "type": "boolean",
+ "default": false
+ },
+ "users": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/User"
+ }
+ },
+ "userId": {
+ "type": ["integer", "null"]
+ },
+ "team": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Team"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "bookings": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Booking"
+ }
+ },
+ "availability": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Availability"
+ }
+ },
+ "webhooks": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Webhook"
+ }
+ },
+ "destinationCalendar": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/DestinationCalendar"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "eventName": {
+ "type": ["string", "null"]
+ },
+ "customInputs": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/EventTypeCustomInput"
+ }
+ },
+ "timeZone": {
+ "type": ["string", "null"]
+ },
+ "periodType": {
+ "type": "string",
+ "default": "UNLIMITED",
+ "enum": ["UNLIMITED", "ROLLING", "RANGE"]
+ },
+ "periodStartDate": {
+ "type": ["string", "null"],
+ "format": "date-time"
+ },
+ "periodEndDate": {
+ "type": ["string", "null"],
+ "format": "date-time"
+ },
+ "periodDays": {
+ "type": ["integer", "null"]
+ },
+ "periodCountCalendarDays": {
+ "type": ["boolean", "null"]
+ },
+ "requiresConfirmation": {
+ "type": "boolean",
+ "default": false
+ },
+ "disableGuests": {
+ "type": "boolean",
+ "default": false
+ },
+ "minimumBookingNotice": {
+ "type": "integer",
+ "default": 120
+ },
+ "beforeEventBuffer": {
+ "type": "integer",
+ "default": 0
+ },
+ "afterEventBuffer": {
+ "type": "integer",
+ "default": 0
+ },
+ "schedulingType": {
+ "type": ["string", "null"],
+ "enum": ["ROUND_ROBIN", "COLLECTIVE"]
+ },
+ "schedule": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Schedule"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "price": {
+ "type": "integer",
+ "default": 0
+ },
+ "currency": {
+ "type": "string",
+ "default": "usd"
+ },
+ "slotInterval": {
+ "type": ["integer", "null"]
+ },
+ "metadata": {
+ "type": ["number", "string", "boolean", "object", "array", "null"]
+ }
+ }
+ },
+ "Credential": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "type": {
+ "type": "string"
+ },
+ "key": {
+ "type": ["number", "string", "boolean", "object", "array", "null"]
+ },
+ "user": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/User"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ "DestinationCalendar": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "integration": {
+ "type": "string"
+ },
+ "externalId": {
+ "type": "string"
+ },
+ "user": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/User"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "booking": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Booking"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "eventType": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/EventType"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ "User": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "username": {
+ "type": ["string", "null"]
+ },
+ "name": {
+ "type": ["string", "null"]
+ },
+ "email": {
+ "type": "string",
+ "description": "@zod.email()"
+ },
+ "emailVerified": {
+ "type": ["string", "null"],
+ "format": "date-time"
+ },
+ "password": {
+ "type": ["string", "null"]
+ },
+ "bio": {
+ "type": ["string", "null"]
+ },
+ "avatar": {
+ "type": ["string", "null"]
+ },
+ "timeZone": {
+ "type": "string",
+ "default": "Europe/London"
+ },
+ "weekStart": {
+ "type": "string",
+ "default": "Sunday"
+ },
+ "startTime": {
+ "type": "integer",
+ "default": 0
+ },
+ "endTime": {
+ "type": "integer",
+ "default": 1440
+ },
+ "bufferTime": {
+ "type": "integer",
+ "default": 0
+ },
+ "hideBranding": {
+ "type": "boolean",
+ "default": false
+ },
+ "theme": {
+ "type": ["string", "null"]
+ },
+ "createdDate": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "trialEndsAt": {
+ "type": ["string", "null"],
+ "format": "date-time"
+ },
+ "eventTypes": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/EventType"
+ }
+ },
+ "credentials": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Credential"
+ }
+ },
+ "teams": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Membership"
+ }
+ },
+ "bookings": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Booking"
+ }
+ },
+ "schedules": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Schedule"
+ }
+ },
+ "defaultScheduleId": {
+ "type": ["integer", "null"]
+ },
+ "selectedCalendars": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/SelectedCalendar"
+ }
+ },
+ "completedOnboarding": {
+ "type": "boolean",
+ "default": false
+ },
+ "locale": {
+ "type": ["string", "null"]
+ },
+ "timeFormat": {
+ "type": ["integer", "null"],
+ "default": 12
+ },
+ "twoFactorSecret": {
+ "type": ["string", "null"]
+ },
+ "twoFactorEnabled": {
+ "type": "boolean",
+ "default": false
+ },
+ "identityProvider": {
+ "type": "string",
+ "default": "CAL",
+ "enum": ["CAL", "GOOGLE", "SAML"]
+ },
+ "identityProviderId": {
+ "type": ["string", "null"]
+ },
+ "availability": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Availability"
+ }
+ },
+ "invitedTo": {
+ "type": ["integer", "null"]
+ },
+ "plan": {
+ "type": "string",
+ "default": "TRIAL",
+ "enum": ["FREE", "TRIAL", "PRO"]
+ },
+ "webhooks": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Webhook"
+ }
+ },
+ "brandColor": {
+ "type": "string",
+ "default": "#292929"
+ },
+ "darkBrandColor": {
+ "type": "string",
+ "default": "#fafafa"
+ },
+ "destinationCalendar": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/DestinationCalendar"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "away": {
+ "type": "boolean",
+ "default": false
+ },
+ "metadata": {
+ "type": ["number", "string", "boolean", "object", "array", "null"]
+ },
+ "verified": {
+ "type": ["boolean", "null"],
+ "default": false
+ },
+ "apiKeys": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ApiKey"
+ }
+ }
+ }
+ },
+ "Team": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": ["string", "null"]
+ },
+ "slug": {
+ "type": ["string", "null"]
+ },
+ "logo": {
+ "type": ["string", "null"]
+ },
+ "bio": {
+ "type": ["string", "null"]
+ },
+ "hideBranding": {
+ "type": "boolean",
+ "default": false
+ },
+ "members": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Membership"
+ }
+ },
+ "eventTypes": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/EventType"
+ }
+ }
+ }
+ },
+ "Membership": {
+ "type": "object",
+ "properties": {
+ "accepted": {
+ "type": "boolean",
+ "default": false
+ },
+ "role": {
+ "type": "string",
+ "enum": ["MEMBER", "ADMIN", "OWNER"]
+ },
+ "team": {
+ "$ref": "#/definitions/Team"
+ },
+ "user": {
+ "$ref": "#/definitions/User"
+ }
+ }
+ },
+ "VerificationRequest": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "identifier": {
+ "type": "string"
+ },
+ "token": {
+ "type": "string"
+ },
+ "expires": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "BookingReference": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "type": {
+ "type": "string"
+ },
+ "uid": {
+ "type": "string"
+ },
+ "meetingId": {
+ "type": ["string", "null"]
+ },
+ "meetingPassword": {
+ "type": ["string", "null"]
+ },
+ "meetingUrl": {
+ "type": ["string", "null"]
+ },
+ "booking": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Booking"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ "Attendee": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "email": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "timeZone": {
+ "type": "string"
+ },
+ "locale": {
+ "type": ["string", "null"],
+ "default": "en"
+ },
+ "booking": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Booking"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ "DailyEventReference": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "dailyurl": {
+ "type": "string",
+ "default": "dailycallurl"
+ },
+ "dailytoken": {
+ "type": "string",
+ "default": "dailytoken"
+ },
+ "booking": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Booking"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ "Booking": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "uid": {
+ "type": "string"
+ },
+ "user": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/User"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "references": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/BookingReference"
+ }
+ },
+ "eventType": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/EventType"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": ["string", "null"]
+ },
+ "startTime": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "endTime": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "attendees": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Attendee"
+ }
+ },
+ "location": {
+ "type": ["string", "null"]
+ },
+ "dailyRef": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/DailyEventReference"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updatedAt": {
+ "type": ["string", "null"],
+ "format": "date-time"
+ },
+ "confirmed": {
+ "type": "boolean",
+ "default": true
+ },
+ "rejected": {
+ "type": "boolean",
+ "default": false
+ },
+ "status": {
+ "type": "string",
+ "default": "ACCEPTED",
+ "enum": ["CANCELLED", "ACCEPTED", "REJECTED", "PENDING"]
+ },
+ "paid": {
+ "type": "boolean",
+ "default": false
+ },
+ "payment": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Payment"
+ }
+ },
+ "destinationCalendar": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/DestinationCalendar"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "cancellationReason": {
+ "type": ["string", "null"]
+ },
+ "rejectionReason": {
+ "type": ["string", "null"]
+ }
+ }
+ },
+ "Schedule": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "user": {
+ "$ref": "#/definitions/User"
+ },
+ "eventType": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/EventType"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "name": {
+ "type": "string"
+ },
+ "timeZone": {
+ "type": ["string", "null"]
+ },
+ "availability": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Availability"
+ }
+ }
+ }
+ },
+ "Availability": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "user": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/User"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "eventType": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/EventType"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "days": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "startTime": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "endTime": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "date": {
+ "type": ["string", "null"],
+ "format": "date-time"
+ },
+ "Schedule": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Schedule"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ "SelectedCalendar": {
+ "type": "object",
+ "properties": {
+ "user": {
+ "$ref": "#/definitions/User"
+ },
+ "integration": {
+ "type": "string"
+ },
+ "externalId": {
+ "type": "string"
+ }
+ }
+ },
+ "EventTypeCustomInput": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "eventType": {
+ "$ref": "#/definitions/EventType"
+ },
+ "label": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["TEXT", "TEXTLONG", "NUMBER", "BOOL"]
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "placeholder": {
+ "type": "string",
+ "default": ""
+ }
+ }
+ },
+ "ResetPasswordRequest": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "email": {
+ "type": "string"
+ },
+ "expires": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "ReminderMail": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "referenceId": {
+ "type": "integer"
+ },
+ "reminderType": {
+ "type": "string",
+ "enum": ["PENDING_BOOKING_CONFIRMATION"]
+ },
+ "elapsedMinutes": {
+ "type": "integer"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "Payment": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "uid": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["STRIPE"]
+ },
+ "booking": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Booking"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "amount": {
+ "type": "integer"
+ },
+ "fee": {
+ "type": "integer"
+ },
+ "currency": {
+ "type": "string"
+ },
+ "success": {
+ "type": "boolean"
+ },
+ "refunded": {
+ "type": "boolean"
+ },
+ "data": {
+ "type": ["number", "string", "boolean", "object", "array", "null"]
+ },
+ "externalId": {
+ "type": "string"
+ }
+ }
+ },
+ "Webhook": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "subscriberUrl": {
+ "type": "string"
+ },
+ "payloadTemplate": {
+ "type": ["string", "null"]
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "active": {
+ "type": "boolean",
+ "default": true
+ },
+ "eventTriggers": {
+ "type": "array",
+ "enum": ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"]
+ },
+ "user": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/User"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "eventType": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/EventType"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ "ApiKey": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "user": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/User"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "expiresAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "note": {
+ "type": ["string", "null"]
+ }
+ }
+ }
+ },
+ "type": "object",
+ "properties": {
+ "eventType": {
+ "$ref": "#/definitions/EventType"
+ },
+ "credential": {
+ "$ref": "#/definitions/Credential"
+ },
+ "destinationCalendar": {
+ "$ref": "#/definitions/DestinationCalendar"
+ },
+ "user": {
+ "$ref": "#/definitions/User"
+ },
+ "team": {
+ "$ref": "#/definitions/Team"
+ },
+ "membership": {
+ "$ref": "#/definitions/Membership"
+ },
+ "verificationRequest": {
+ "$ref": "#/definitions/VerificationRequest"
+ },
+ "bookingReference": {
+ "$ref": "#/definitions/BookingReference"
+ },
+ "attendee": {
+ "$ref": "#/definitions/Attendee"
+ },
+ "dailyEventReference": {
+ "$ref": "#/definitions/DailyEventReference"
+ },
+ "booking": {
+ "$ref": "#/definitions/Booking"
+ },
+ "schedule": {
+ "$ref": "#/definitions/Schedule"
+ },
+ "availability": {
+ "$ref": "#/definitions/Availability"
+ },
+ "selectedCalendar": {
+ "$ref": "#/definitions/SelectedCalendar"
+ },
+ "eventTypeCustomInput": {
+ "$ref": "#/definitions/EventTypeCustomInput"
+ },
+ "resetPasswordRequest": {
+ "$ref": "#/definitions/ResetPasswordRequest"
+ },
+ "reminderMail": {
+ "$ref": "#/definitions/ReminderMail"
+ },
+ "payment": {
+ "$ref": "#/definitions/Payment"
+ },
+ "webhook": {
+ "$ref": "#/definitions/Webhook"
+ },
+ "apiKey": {
+ "$ref": "#/definitions/ApiKey"
+ }
+ }
+}
diff --git a/packages/prisma/migrations/20220413002425_adds_api_keys/migration.sql b/packages/prisma/migrations/20220413002425_adds_api_keys/migration.sql
new file mode 100644
index 00000000..4d5f1086
--- /dev/null
+++ b/packages/prisma/migrations/20220413002425_adds_api_keys/migration.sql
@@ -0,0 +1,21 @@
+-- CreateTable
+CREATE TABLE "ApiKey" (
+ "id" TEXT NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "note" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "expiresAt" TIMESTAMP(3),
+ "lastUsedAt" TIMESTAMP(3),
+ "hashedKey" TEXT NOT NULL,
+
+ CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ApiKey_id_key" ON "ApiKey"("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ApiKey_hashedKey_key" ON "ApiKey"("hashedKey");
+
+-- AddForeignKey
+ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 9e206f2d..602dcccb 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -154,6 +154,7 @@ model User {
allowDynamicBooking Boolean? @default(true)
metadata Json?
verified Boolean? @default(false)
+ apiKeys ApiKey[]
@@map(name: "users")
}
@@ -373,3 +374,14 @@ model Webhook {
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
}
+
+model ApiKey {
+ id String @id @unique @default(cuid())
+ userId Int
+ note String?
+ createdAt DateTime @default(now())
+ expiresAt DateTime?
+ lastUsedAt DateTime?
+ hashedKey String @unique()
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
+}