diff --git a/apps/web/pages/api/auth/oauth/me.ts b/apps/web/pages/api/auth/oauth/me.ts
new file mode 100644
index 0000000000..81aaf6e101
--- /dev/null
+++ b/apps/web/pages/api/auth/oauth/me.ts
@@ -0,0 +1,14 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+
+import isAuthorized from "@calcom/features/auth/lib/oAuthAuthorization";
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const requriedScopes = ["READ_PROFILE"];
+
+ const account = await isAuthorized(req, requriedScopes);
+
+ if (!account) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ return res.status(201).json({ username: account.name });
+}
diff --git a/apps/web/pages/api/auth/oauth/refreshToken.ts b/apps/web/pages/api/auth/oauth/refreshToken.ts
new file mode 100644
index 0000000000..a302cc8bf6
--- /dev/null
+++ b/apps/web/pages/api/auth/oauth/refreshToken.ts
@@ -0,0 +1,68 @@
+import jwt from "jsonwebtoken";
+import type { NextApiRequest, NextApiResponse } from "next";
+import type { OAuthTokenPayload } from "pages/api/auth/oauth/token";
+
+import prisma from "@calcom/prisma";
+import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== "POST") {
+ res.status(405).json({ message: "Invalid method" });
+ return;
+ }
+
+ const refreshToken = req.headers.authorization?.split(" ")[1] || "";
+
+ const { client_id, client_secret, grant_type } = req.body;
+
+ if (grant_type !== "refresh_token") {
+ res.status(400).json({ message: "grant type invalid" });
+ return;
+ }
+
+ const [hashedSecret] = generateSecret(client_secret);
+
+ const client = await prisma.oAuthClient.findFirst({
+ where: {
+ clientId: client_id,
+ clientSecret: hashedSecret,
+ },
+ select: {
+ redirectUri: true,
+ },
+ });
+
+ if (!client) {
+ res.status(401).json({ message: "Unauthorized" });
+ return;
+ }
+
+ const secretKey = process.env.CALENDSO_ENCRYPTION_KEY || "";
+
+ let decodedRefreshToken: OAuthTokenPayload;
+
+ try {
+ decodedRefreshToken = jwt.verify(refreshToken, secretKey) as OAuthTokenPayload;
+ } catch {
+ res.status(401).json({ message: "Unauthorized" });
+ return;
+ }
+
+ if (!decodedRefreshToken || decodedRefreshToken.token_type !== "Refresh Token") {
+ res.status(401).json({ message: "Unauthorized" });
+ return;
+ }
+
+ const payload: OAuthTokenPayload = {
+ userId: decodedRefreshToken.userId,
+ scope: decodedRefreshToken.scope,
+ token_type: "Access Token",
+ clientId: client_id,
+ };
+
+ const access_token = jwt.sign(payload, secretKey, {
+ expiresIn: 1800, // 30 min
+ });
+
+ res.status(200).json({ access_token });
+}
diff --git a/apps/web/pages/api/auth/oauth/token.ts b/apps/web/pages/api/auth/oauth/token.ts
new file mode 100644
index 0000000000..6ad4d1e071
--- /dev/null
+++ b/apps/web/pages/api/auth/oauth/token.ts
@@ -0,0 +1,104 @@
+import jwt from "jsonwebtoken";
+import type { NextApiRequest, NextApiResponse } from "next";
+
+import prisma from "@calcom/prisma";
+import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
+
+export type OAuthTokenPayload = {
+ userId?: number | null;
+ teamId?: number | null;
+ token_type: string;
+ scope: string[];
+ clientId: string;
+};
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== "POST") {
+ res.status(405).json({ message: "Invalid method" });
+ return;
+ }
+
+ const { code, client_id, client_secret, grant_type, redirect_uri } = req.body;
+
+ if (grant_type !== "authorization_code") {
+ res.status(400).json({ message: "grant_type invalid" });
+ return;
+ }
+
+ const [hashedSecret] = generateSecret(client_secret);
+
+ const client = await prisma.oAuthClient.findFirst({
+ where: {
+ clientId: client_id,
+ clientSecret: hashedSecret,
+ },
+ select: {
+ redirectUri: true,
+ },
+ });
+
+ if (!client || client.redirectUri !== redirect_uri) {
+ res.status(401).json({ message: "Unauthorized" });
+ return;
+ }
+
+ const accessCode = await prisma.accessCode.findFirst({
+ where: {
+ code: code,
+ clientId: client_id,
+ expiresAt: {
+ gt: new Date(),
+ },
+ },
+ });
+
+ //delete all expired accessCodes + the one that is used here
+ await prisma.accessCode.deleteMany({
+ where: {
+ OR: [
+ {
+ expiresAt: {
+ lt: new Date(),
+ },
+ },
+ {
+ code: code,
+ clientId: client_id,
+ },
+ ],
+ },
+ });
+
+ if (!accessCode) {
+ res.status(401).json({ message: "Unauthorized" });
+ return;
+ }
+
+ const secretKey = process.env.CALENDSO_ENCRYPTION_KEY || "";
+
+ const payloadAuthToken: OAuthTokenPayload = {
+ userId: accessCode.userId,
+ teamId: accessCode.teamId,
+ scope: accessCode.scopes,
+ token_type: "Access Token",
+ clientId: client_id,
+ };
+
+ const payloadRefreshToken: OAuthTokenPayload = {
+ userId: accessCode.userId,
+ teamId: accessCode.teamId,
+ scope: accessCode.scopes,
+ token_type: "Refresh Token",
+ clientId: client_id,
+ };
+
+ const access_token = jwt.sign(payloadAuthToken, secretKey, {
+ expiresIn: 1800, // 30 min
+ });
+
+ const refresh_token = jwt.sign(payloadRefreshToken, secretKey, {
+ expiresIn: 30 * 24 * 60 * 60, // 30 days
+ });
+
+ res.status(200).json({ access_token, refresh_token });
+}
diff --git a/apps/web/pages/api/trpc/oAuth/[trpc].ts b/apps/web/pages/api/trpc/oAuth/[trpc].ts
new file mode 100644
index 0000000000..d4569d82f3
--- /dev/null
+++ b/apps/web/pages/api/trpc/oAuth/[trpc].ts
@@ -0,0 +1,4 @@
+import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
+import { oAuthRouter } from "@calcom/trpc/server/routers/viewer/oAuth/_router";
+
+export default createNextApiHandler(oAuthRouter);
diff --git a/apps/web/pages/auth/oauth2/authorize.tsx b/apps/web/pages/auth/oauth2/authorize.tsx
new file mode 100644
index 0000000000..6b2c276aac
--- /dev/null
+++ b/apps/web/pages/auth/oauth2/authorize.tsx
@@ -0,0 +1,179 @@
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useSearchParams } from "next/navigation";
+import { useState, useEffect } from "react";
+
+import { APP_NAME } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Avatar, Button, Select } from "@calcom/ui";
+import { Plus, Info } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function Authorize() {
+ const { t } = useLocale();
+ const { status } = useSession();
+
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const client_id = searchParams?.get("client_id") as string;
+ const state = searchParams?.get("state") as string;
+ const scope = searchParams?.get("scope") as string;
+
+ const queryString = searchParams.toString();
+
+ const [selectedAccount, setSelectedAccount] = useState<{ value: string; label: string } | null>();
+ const scopes = scope ? scope.toString().split(",") : [];
+
+ const { data: client, isLoading: isLoadingGetClient } = trpc.viewer.oAuth.getClient.useQuery(
+ {
+ clientId: client_id as string,
+ },
+ {
+ enabled: status !== "loading",
+ }
+ );
+
+ const { data, isLoading: isLoadingProfiles } = trpc.viewer.teamsAndUserProfilesQuery.useQuery();
+
+ const generateAuthCodeMutation = trpc.viewer.oAuth.generateAuthCode.useMutation({
+ onSuccess: (data) => {
+ window.location.href = `${client?.redirectUri}?code=${data.authorizationCode}&state=${state}`;
+ },
+ });
+
+ const mappedProfiles = data
+ ? data
+ .filter((profile) => !profile.readOnly)
+ .map((profile) => ({
+ label: profile.name || profile.slug || "",
+ value: profile.slug || "",
+ }))
+ : [];
+
+ useEffect(() => {
+ if (mappedProfiles.length > 0) {
+ setSelectedAccount(mappedProfiles[0]);
+ }
+ }, [isLoadingProfiles]);
+
+ useEffect(() => {
+ if (status === "unauthenticated") {
+ const urlSearchParams = new URLSearchParams({
+ callbackUrl: `auth/oauth2/authorize?${queryString}`,
+ });
+ router.replace(`/auth/login?${urlSearchParams.toString()}`);
+ }
+ }, [status]);
+
+ const isLoading = isLoadingGetClient || isLoadingProfiles || status !== "authenticated";
+
+ if (isLoading) {
+ return <>>;
+ }
+
+ if (!client) {
+ return
{t("unauthorized")}
;
+ }
+
+ return (
+
+
+
+
}
+ className="items-center"
+ imageSrc={client.logo}
+ size="lg"
+ />
+
+
+
+
+
+
+
+
+
+ {t("access_cal_account", { clientName: client.name, appName: APP_NAME })}
+
+
{t("select_account_team")}
+
+
+ );
+}
+
+Authorize.PageWrapper = PageWrapper;
diff --git a/apps/web/pages/settings/admin/oAuth/index.tsx b/apps/web/pages/settings/admin/oAuth/index.tsx
new file mode 100644
index 0000000000..dec29e4af5
--- /dev/null
+++ b/apps/web/pages/settings/admin/oAuth/index.tsx
@@ -0,0 +1,11 @@
+import PageWrapper from "@components/PageWrapper";
+import { getLayout } from "@components/auth/layouts/AdminLayout";
+
+import OAuthView from "./oAuthView";
+
+const OAuthPage = () => ;
+
+OAuthPage.getLayout = getLayout;
+OAuthPage.PageWrapper = PageWrapper;
+
+export default OAuthPage;
diff --git a/apps/web/pages/settings/admin/oAuth/oAuthView.tsx b/apps/web/pages/settings/admin/oAuth/oAuthView.tsx
new file mode 100644
index 0000000000..a6a16463cf
--- /dev/null
+++ b/apps/web/pages/settings/admin/oAuth/oAuthView.tsx
@@ -0,0 +1,151 @@
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+import { Meta, Form, Button, TextField, showToast, Tooltip, ImageUploader, Avatar } from "@calcom/ui";
+import { Clipboard } from "@calcom/ui/components/icon";
+import { Plus } from "@calcom/ui/components/icon";
+
+type FormValues = {
+ name: string;
+ redirectUri: string;
+ logo: string;
+};
+
+export default function OAuthView() {
+ const oAuthForm = useForm();
+ const [clientSecret, setClientSecret] = useState("");
+ const [clientId, setClientId] = useState("");
+ const [logo, setLogo] = useState("");
+ const { t } = useLocale();
+
+ const mutation = trpc.viewer.oAuth.addClient.useMutation({
+ onSuccess: async (data) => {
+ setClientSecret(data.clientSecret);
+ setClientId(data.clientId);
+ showToast(`Successfully added ${data.name} as new client`, "success");
+ },
+ onError: (error) => {
+ showToast(`Adding clientfailed: ${error.message}`, "error");
+ },
+ });
+
+ return (
+
+
+ {!clientId ? (
+
+ ) : (
+
+
{oAuthForm.getValues("name")}
+
Client Id
+
+
+ {" "}
+ {clientId}
+
+
+
+
+
+ {clientSecret ? (
+ <>
+
Client Secret
+
+
+ {" "}
+ {clientSecret}
+
+
+
+
+
+
{t("copy_client_secret_info")}
+ >
+ ) : (
+ <>>
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/apps/web/playwright/oauth-provider.e2e.ts b/apps/web/playwright/oauth-provider.e2e.ts
new file mode 100644
index 0000000000..2d8c73cb89
--- /dev/null
+++ b/apps/web/playwright/oauth-provider.e2e.ts
@@ -0,0 +1,227 @@
+import { expect } from "@playwright/test";
+import { randomBytes } from "crypto";
+
+import { WEBAPP_URL } from "@calcom/lib/constants";
+import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
+
+import { test } from "./lib/fixtures";
+
+test.afterEach(async ({ users }) => {
+ await users.deleteAll();
+});
+
+let client: {
+ clientId: string;
+ redirectUri: string;
+ orginalSecret: string;
+ name: string;
+ clientSecret: string;
+ logo: string | null;
+};
+
+test.describe("OAuth Provider", () => {
+ test.beforeAll(async () => {
+ client = await createTestCLient();
+ });
+ test("should create valid access toke & refresh token for user", async ({ page, users }) => {
+ const user = await users.create({ username: "test user", name: "test user" });
+ await user.apiLogin();
+
+ await page.goto(
+ `auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
+ );
+
+ await page.waitForLoadState("networkidle");
+ await page.getByTestId("allow-button").click();
+
+ await page.waitForFunction(() => {
+ return window.location.href.startsWith("https://example.com");
+ });
+
+ const url = new URL(page.url());
+
+ // authorization code that is returned to client with redirect uri
+ const code = url.searchParams.get("code");
+
+ // request token with authorization code
+ const tokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/token`, {
+ body: JSON.stringify({
+ code,
+ client_id: client.clientId,
+ client_secret: client.orginalSecret,
+ grant_type: "authorization_code",
+ redirect_uri: client.redirectUri,
+ }),
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const tokenData = await tokenResponse.json();
+
+ // test if token is valid
+ const meResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + tokenData.access_token,
+ },
+ });
+
+ const meData = await meResponse.json();
+
+ // check if user access token is valid
+ expect(meData.username.startsWith("test user")).toBe(true);
+
+ // request new token with refresh token
+ const refreshTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/refreshToken`, {
+ body: JSON.stringify({
+ refresh_token: tokenData.refresh_token,
+ client_id: client.clientId,
+ client_secret: client.orginalSecret,
+ grant_type: "refresh_token",
+ }),
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const refreshTokenData = await refreshTokenResponse.json();
+
+ expect(refreshTokenData.access_token).not.toBe(tokenData.access_token);
+
+ const validTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + tokenData.access_token,
+ },
+ });
+
+ expect(meData.username.startsWith("test user")).toBe(true);
+ });
+
+ test("should create valid access toke & refresh token for team", async ({ page, users }) => {
+ const user = await users.create({ username: "test user", name: "test user" }, { hasTeam: true });
+ await user.apiLogin();
+
+ await page.goto(
+ `auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
+ );
+
+ await page.waitForLoadState("networkidle");
+
+ await page.locator("#account-select").click();
+
+ await page.locator("#react-select-2-option-1").click();
+
+ await page.getByTestId("allow-button").click();
+
+ await page.waitForFunction(() => {
+ return window.location.href.startsWith("https://example.com");
+ });
+
+ const url = new URL(page.url());
+
+ // authorization code that is returned to client with redirect uri
+ const code = url.searchParams.get("code");
+
+ // request token with authorization code
+ const tokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/token`, {
+ body: JSON.stringify({
+ code,
+ client_id: client.clientId,
+ client_secret: client.orginalSecret,
+ grant_type: "authorization_code",
+ redirect_uri: client.redirectUri,
+ }),
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const tokenData = await tokenResponse.json();
+
+ // test if token is valid
+ const meResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + tokenData.access_token,
+ },
+ });
+
+ const meData = await meResponse.json();
+
+ // check if team access token is valid
+ expect(meData.username.endsWith("Team Team")).toBe(true);
+
+ // request new token with refresh token
+ const refreshTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/refreshToken`, {
+ body: JSON.stringify({
+ refresh_token: tokenData.refresh_token,
+ client_id: client.clientId,
+ client_secret: client.orginalSecret,
+ grant_type: "refresh_token",
+ }),
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const refreshTokenData = await refreshTokenResponse.json();
+
+ expect(refreshTokenData.access_token).not.toBe(tokenData.access_token);
+
+ const validTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/me`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + tokenData.access_token,
+ },
+ });
+
+ expect(meData.username.endsWith("Team Team")).toBe(true);
+ });
+
+ test("redirect not logged-in users to login page and after forward to authorization page", async ({
+ page,
+ users,
+ }) => {
+ const user = await users.create({ username: "test-user", name: "test user" });
+
+ await page.goto(
+ `auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
+ );
+
+ // check if user is redirected to login page
+ await expect(page.getByRole("heading", { name: "Welcome back" })).toBeVisible();
+ await page.locator("#email").fill(user.email);
+ await page.locator("#password").fill(user.username || "");
+ await page.locator('[type="submit"]').click();
+
+ await page.waitForSelector("#account-select");
+
+ await expect(page.getByText("test user")).toBeVisible();
+ });
+});
+
+const createTestCLient = async () => {
+ const [hashedSecret, secret] = generateSecret();
+ const clientId = randomBytes(32).toString("hex");
+
+ const client = await prisma.oAuthClient.create({
+ data: {
+ name: "Test Client",
+ clientId,
+ clientSecret: hashedSecret,
+ redirectUri: "https://example.com",
+ },
+ });
+
+ return { ...client, orginalSecret: secret };
+};
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 0029be106a..dbf6790069 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -2054,15 +2054,33 @@
"team_no_event_types": "This team has no event types",
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
"include_calendar_event": "Include calendar event",
+ "oAuth": "OAuth",
"recently_added":"Recently added",
"no_members_found": "No members found",
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
"availability_schedules":"Availability Schedules",
+ "unauthorized":"Unauthorized",
+ "access_cal_account": "{{clientName}} would like access to your {{appName}} account",
+ "select_account_team": "Select account or team",
+ "allow_client_to": "This will allow {{clientName}} to",
+ "associate_with_cal_account":"Associate you with your personal info from {{clientName}}",
+ "see_personal_info":"See your personal info, including any personal info you've made publicly available",
+ "see_primary_email_address":"See your primary email address",
+ "connect_installed_apps":"Connect to your installed apps",
+ "access_event_type": "Read, edit, delete your event-types",
+ "access_availability": "Read, edit, delete your availability",
+ "access_bookings": "Read, edit, delete your bookings",
+ "allow_client_to_do": "Allow {{clientName}} to do this?",
+ "oauth_access_information": "By clicking allow, you allow this app to use your information in accordance with their terms of service and privacy policy. You can remove access in the {{appName}} App Store.",
+ "allow": "Allow",
"view_only_edit_availability_not_onboarded":"This user has not completed onboarding. You will not be able to set their availability until they have completed onboarding.",
"view_only_edit_availability":"You are viewing this user's availability. You can only edit your own availability.",
"edit_users_availability":"Edit user's availability: {{username}}",
"resend_invitation": "Resend invitation",
"invitation_resent": "The invitation was resent.",
+ "add_client": "Add client",
+ "copy_client_secret_info": "After copying the secret you won't be able to view it anymore",
+ "add_new_client": "Add new Client",
"this_app_is_not_setup_already": "This app has not been setup yet",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
diff --git a/package.json b/package.json
index badd51a1ea..6511ddb568 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"@playwright/test": "^1.31.2",
"@snaplet/copycat": "^0.3.0",
"@testing-library/jest-dom": "^5.16.5",
+ "@types/jsonwebtoken": "^9.0.3",
"c8": "^7.13.0",
"dotenv-checker": "^1.1.5",
"husky": "^8.0.0",
diff --git a/packages/app-store/zapier/api/subscriptions/addSubscription.ts b/packages/app-store/zapier/api/subscriptions/addSubscription.ts
index 675f161be1..fc05c093a4 100644
--- a/packages/app-store/zapier/api/subscriptions/addSubscription.ts
+++ b/packages/app-store/zapier/api/subscriptions/addSubscription.ts
@@ -1,26 +1,16 @@
import type { NextApiRequest, NextApiResponse } from "next";
-import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { addSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
+import { validateAccountOrApiKey } from "../../lib/validateAccountOrApiKey";
+
async function handler(req: NextApiRequest, res: NextApiResponse) {
- const apiKey = req.query.apiKey as string;
-
- if (!apiKey) {
- return res.status(401).json({ message: "No API key provided" });
- }
-
- const validKey = await findValidApiKey(apiKey, "zapier");
-
- if (!validKey) {
- return res.status(401).json({ message: "API key not valid" });
- }
-
const { subscriberUrl, triggerEvent } = req.body;
-
+ const { account, appApiKey } = await validateAccountOrApiKey(req, ["READ_BOOKING", "READ_PROFILE"]);
const createAppSubscription = await addSubscription({
- appApiKey: validKey,
+ appApiKey,
+ account,
triggerEvent: triggerEvent,
subscriberUrl: subscriberUrl,
appId: "zapier",
diff --git a/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts b/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts
index 81469a30ff..699f5fc3d5 100644
--- a/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts
+++ b/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts
@@ -1,30 +1,24 @@
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
-import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { deleteSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
+import { validateAccountOrApiKey } from "../../lib/validateAccountOrApiKey";
+
const querySchema = z.object({
apiKey: z.string(),
id: z.string(),
});
async function handler(req: NextApiRequest, res: NextApiResponse) {
- const { apiKey, id } = querySchema.parse(req.query);
+ const { id } = querySchema.parse(req.query);
- if (!apiKey) {
- return res.status(401).json({ message: "No API key provided" });
- }
-
- const validKey = await findValidApiKey(apiKey, "zapier");
-
- if (!validKey) {
- return res.status(401).json({ message: "API key not valid" });
- }
+ const { account, appApiKey } = await validateAccountOrApiKey(req, ["READ_BOOKING", "READ_PROFILE"]);
const deleteEventSubscription = await deleteSubscription({
- appApiKey: validKey,
+ appApiKey,
+ account,
webhookId: id,
appId: "zapier",
});
diff --git a/packages/app-store/zapier/api/subscriptions/listBookings.ts b/packages/app-store/zapier/api/subscriptions/listBookings.ts
index f3a59c690a..62d74e44c3 100644
--- a/packages/app-store/zapier/api/subscriptions/listBookings.ts
+++ b/packages/app-store/zapier/api/subscriptions/listBookings.ts
@@ -1,29 +1,32 @@
import type { NextApiRequest, NextApiResponse } from "next";
-import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { listBookings } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
+import { validateAccountOrApiKey } from "../../lib/validateAccountOrApiKey";
+
async function handler(req: NextApiRequest, res: NextApiResponse) {
- const apiKey = req.query.apiKey as string;
-
- if (!apiKey) {
- return res.status(401).json({ message: "No API key provided" });
- }
-
- const validKey = await findValidApiKey(apiKey, "zapier");
-
- if (!validKey) {
- return res.status(401).json({ message: "API key not valid" });
- }
-
- const bookings = await listBookings(validKey);
+ const { account: authorizedAccount, appApiKey: validKey } = await validateAccountOrApiKey(req, [
+ "READ_BOOKING",
+ ]);
+ const bookings = await listBookings(validKey, authorizedAccount);
if (!bookings) {
return res.status(500).json({ message: "Unable to get bookings." });
}
if (bookings.length === 0) {
- const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
+ const userInfo = validKey
+ ? validKey.userId
+ : authorizedAccount && !authorizedAccount.isTeam
+ ? authorizedAccount.name
+ : null;
+ const teamInfo = validKey
+ ? validKey.teamId
+ : authorizedAccount && authorizedAccount.isTeam
+ ? authorizedAccount.name
+ : null;
+
+ const requested = teamInfo ? "team: " + teamInfo : "user: " + userInfo;
return res.status(404).json({
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
});
diff --git a/packages/app-store/zapier/lib/validateAccountOrApiKey.ts b/packages/app-store/zapier/lib/validateAccountOrApiKey.ts
new file mode 100644
index 0000000000..151be97071
--- /dev/null
+++ b/packages/app-store/zapier/lib/validateAccountOrApiKey.ts
@@ -0,0 +1,19 @@
+import type { NextApiRequest } from "next";
+
+import isAuthorized from "@calcom/features/auth/lib/oAuthAuthorization";
+import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
+import { HttpError } from "@calcom/lib/http-error";
+
+export async function validateAccountOrApiKey(req: NextApiRequest, requiredScopes: string[] = []) {
+ const apiKey = req.query.apiKey as string;
+
+ if (!apiKey) {
+ const authorizedAccount = await isAuthorized(req, requiredScopes);
+ if (!authorizedAccount) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
+ return { account: authorizedAccount, appApiKey: undefined };
+ }
+
+ const validKey = await findValidApiKey(apiKey, "zapier");
+ if (!validKey) throw new HttpError({ statusCode: 401, message: "API key not valid" });
+ return { account: null, appApiKey: validKey };
+}
diff --git a/packages/embeds/embed-core/playwright/tests/embed-pages.e2e.ts b/packages/embeds/embed-core/playwright/tests/embed-pages.e2e.ts
index 3133d350db..a392641e01 100644
--- a/packages/embeds/embed-core/playwright/tests/embed-pages.e2e.ts
+++ b/packages/embeds/embed-core/playwright/tests/embed-pages.e2e.ts
@@ -1,5 +1,6 @@
import { expect } from "@playwright/test";
+// eslint-disable-next-line no-restricted-imports
import { test } from "@calcom/web/playwright/lib/fixtures";
import "../../src/types";
diff --git a/packages/embeds/embed-react/test/packaged/api.test.ts b/packages/embeds/embed-react/test/packaged/api.test.ts
index 0334f9dc88..2cd751d010 100644
--- a/packages/embeds/embed-react/test/packaged/api.test.ts
+++ b/packages/embeds/embed-react/test/packaged/api.test.ts
@@ -12,14 +12,14 @@ import { getCalApi } from "@calcom/embed-react";
const api = getCalApi();
test("Check that the API is available", async () => {
- expect(api).toBeDefined()
+ expect(api).toBeDefined();
const awaitedApi = await api;
- awaitedApi('floatingButton', {
- calLink: 'free',
+ awaitedApi("floatingButton", {
+ calLink: "free",
config: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error We are intentionaly testing invalid value
- layout: 'wrongview'
- }
- })
+ layout: "wrongview",
+ },
+ });
});
diff --git a/packages/features/auth/lib/oAuthAuthorization.ts b/packages/features/auth/lib/oAuthAuthorization.ts
new file mode 100644
index 0000000000..2346216285
--- /dev/null
+++ b/packages/features/auth/lib/oAuthAuthorization.ts
@@ -0,0 +1,55 @@
+import jwt from "jsonwebtoken";
+import type { NextApiRequest } from "next";
+
+import prisma from "@calcom/prisma";
+import type { OAuthTokenPayload } from "@calcom/web/pages/api/auth/oauth/token";
+
+export default async function isAuthorized(req: NextApiRequest, requiredScopes: string[] = []) {
+ const token = req.headers.authorization?.split(" ")[1] || "";
+ let decodedToken: OAuthTokenPayload;
+ try {
+ decodedToken = jwt.verify(token, process.env.CALENDSO_ENCRYPTION_KEY || "") as OAuthTokenPayload;
+ } catch {
+ return null;
+ }
+
+ if (!decodedToken) return null;
+ const hasAllRequiredScopes = requiredScopes.every((scope) => decodedToken.scope.includes(scope));
+
+ if (!hasAllRequiredScopes || decodedToken.token_type !== "Access Token") {
+ return null;
+ }
+
+ if (decodedToken.userId) {
+ const user = await prisma.user.findFirst({
+ where: {
+ id: decodedToken.userId,
+ },
+ select: {
+ id: true,
+ username: true,
+ },
+ });
+
+ if (!user) return null;
+
+ return { id: user.id, name: user.username, isTeam: false };
+ }
+
+ if (decodedToken.teamId) {
+ const team = await prisma.team.findFirst({
+ where: {
+ id: decodedToken.teamId,
+ },
+ select: {
+ id: true,
+ name: true,
+ },
+ });
+
+ if (!team) return null;
+ return { ...team, isTeam: true };
+ }
+
+ return null;
+}
diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx
index 2ffe84f7f4..755300bc99 100644
--- a/packages/features/settings/layouts/SettingsLayout.tsx
+++ b/packages/features/settings/layouts/SettingsLayout.tsx
@@ -117,6 +117,7 @@ const tabs: VerticalTabItemProps[] = [
{ name: "apps", href: "/settings/admin/apps/calendar" },
{ name: "users", href: "/settings/admin/users" },
{ name: "organizations", href: "/settings/admin/organizations" },
+ { name: "oAuth", href: "/settings/admin/oAuth" },
],
},
];
diff --git a/packages/features/webhooks/lib/scheduleTrigger.ts b/packages/features/webhooks/lib/scheduleTrigger.ts
index aa5409101b..505a761c55 100644
--- a/packages/features/webhooks/lib/scheduleTrigger.ts
+++ b/packages/features/webhooks/lib/scheduleTrigger.ts
@@ -16,18 +16,27 @@ export async function addSubscription({
triggerEvent,
subscriberUrl,
appId,
+ account,
}: {
- appApiKey: ApiKey;
+ appApiKey?: ApiKey;
triggerEvent: WebhookTriggerEvents;
subscriberUrl: string;
appId: string;
+ account?: {
+ id: number;
+ name: string | null;
+ isTeam: boolean;
+ } | null;
}) {
try {
+ const userId = appApiKey ? appApiKey.userId : account && !account.isTeam ? account.id : null;
+ const teamId = appApiKey ? appApiKey.teamId : account && account.isTeam ? account.id : null;
+
const createSubscription = await prisma.webhook.create({
data: {
id: v4(),
- userId: appApiKey.userId,
- teamId: appApiKey.teamId,
+ userId,
+ teamId,
eventTriggers: [triggerEvent],
subscriberUrl,
active: true,
@@ -38,8 +47,11 @@ export async function addSubscription({
if (triggerEvent === WebhookTriggerEvents.MEETING_ENDED) {
//schedule job for already existing bookings
const where: Prisma.BookingWhereInput = {};
- if (appApiKey.teamId) where.eventType = { teamId: appApiKey.teamId };
- else where.userId = appApiKey.userId;
+ if (teamId) {
+ where.eventType = { teamId };
+ } else {
+ where.userId = userId;
+ }
const bookings = await prisma.booking.findMany({
where: {
...where,
@@ -60,7 +72,10 @@ export async function addSubscription({
return createSubscription;
} catch (error) {
- log.error(`Error creating subscription for user ${appApiKey.userId} and appId ${appApiKey.appId}.`);
+ const userId = appApiKey ? appApiKey.userId : account && !account.isTeam ? account.id : null;
+ const teamId = appApiKey ? appApiKey.teamId : account && account.isTeam ? account.id : null;
+
+ log.error(`Error creating subscription for ${teamId ? `team ${teamId}` : `user ${userId}`}.`);
}
}
@@ -68,10 +83,16 @@ export async function deleteSubscription({
appApiKey,
webhookId,
appId,
+ account,
}: {
- appApiKey: ApiKey;
+ appApiKey?: ApiKey;
webhookId: string;
appId: string;
+ account?: {
+ id: number;
+ name: string | null;
+ isTeam: boolean;
+ } | null;
}) {
try {
const webhook = await prisma.webhook.findFirst({
@@ -82,8 +103,21 @@ export async function deleteSubscription({
if (webhook?.eventTriggers.includes(WebhookTriggerEvents.MEETING_ENDED)) {
const where: Prisma.BookingWhereInput = {};
- if (appApiKey.teamId) where.eventType = { teamId: appApiKey.teamId };
- else where.userId = appApiKey.userId;
+
+ if (appApiKey) {
+ if (appApiKey.teamId) {
+ where.eventType = { teamId: appApiKey.teamId };
+ } else {
+ where.userId = appApiKey.userId;
+ }
+ } else if (account) {
+ if (account.isTeam) {
+ where.eventType = { teamId: account.id };
+ } else {
+ where.userId = account.id;
+ }
+ }
+
const bookingsWithScheduledJobs = await prisma.booking.findMany({
where: {
...where,
@@ -117,22 +151,48 @@ export async function deleteSubscription({
}
return deleteWebhook;
} catch (err) {
+ const userId = appApiKey ? appApiKey.userId : account && !account.isTeam ? account.id : null;
+ const teamId = appApiKey ? appApiKey.teamId : account && account.isTeam ? account.id : null;
+
log.error(
- `Error deleting subscription for user ${appApiKey.userId}, webhookId ${webhookId}, appId ${appId}`
+ `Error deleting subscription for user ${
+ teamId ? `team ${teamId}` : `userId ${userId}`
+ }, webhookId ${webhookId}`
);
}
}
-export async function listBookings(appApiKey: ApiKey) {
+export async function listBookings(
+ appApiKey?: ApiKey,
+ account?: {
+ id: number;
+ name: string | null;
+ isTeam: boolean;
+ } | null
+) {
try {
const where: Prisma.BookingWhereInput = {};
- if (appApiKey.teamId) {
- where.eventType = {
- OR: [{ teamId: appApiKey.teamId }, { parent: { teamId: appApiKey.teamId } }],
- };
- } else {
- where.userId = appApiKey.userId;
+ if (appApiKey) {
+ if (appApiKey.teamId) {
+ where.eventType = {
+ OR: [{ teamId: appApiKey.teamId }, { parent: { teamId: appApiKey.teamId } }],
+ };
+ } else {
+ where.userId = appApiKey.userId;
+ }
+ } else if (account) {
+ if (!account.isTeam) {
+ where.userId = account.id;
+ where.eventType = {
+ teamId: null,
+ };
+ } else {
+ where.eventType = {
+ teamId: account.id,
+ };
+ }
}
+
const bookings = await prisma.booking.findMany({
take: 3,
where: where,
@@ -197,7 +257,10 @@ export async function listBookings(appApiKey: ApiKey) {
return updatedBookings;
} catch (err) {
- log.error(`Error retrieving list of bookings for user ${appApiKey.userId} and appId ${appApiKey.appId}.`);
+ const userId = appApiKey ? appApiKey.userId : account && !account.isTeam ? account.id : null;
+ const teamId = appApiKey ? appApiKey.teamId : account && account.isTeam ? account.id : null;
+
+ log.error(`Error retrieving list of bookings for ${teamId ? `team ${teamId}` : `user ${userId}`}.`);
}
}
diff --git a/packages/prisma/migrations/20230920175742_add_oauth_model/migration.sql b/packages/prisma/migrations/20230920175742_add_oauth_model/migration.sql
new file mode 100644
index 0000000000..fddde37610
--- /dev/null
+++ b/packages/prisma/migrations/20230920175742_add_oauth_model/migration.sql
@@ -0,0 +1,38 @@
+-- CreateEnum
+CREATE TYPE "AccessScope" AS ENUM ('READ_BOOKING', 'READ_PROFILE');
+
+-- CreateTable
+CREATE TABLE "OAuthClient" (
+ "clientId" TEXT NOT NULL,
+ "redirectUri" TEXT NOT NULL,
+ "clientSecret" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "logo" TEXT,
+
+ CONSTRAINT "OAuthClient_pkey" PRIMARY KEY ("clientId")
+);
+
+-- CreateTable
+CREATE TABLE "AccessCode" (
+ "id" SERIAL NOT NULL,
+ "code" TEXT NOT NULL,
+ "clientId" TEXT,
+ "expiresAt" TIMESTAMP(3) NOT NULL,
+ "scopes" "AccessScope"[],
+ "userId" INTEGER,
+ "teamId" INTEGER,
+
+ CONSTRAINT "AccessCode_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "OAuthClient_clientId_key" ON "OAuthClient"("clientId");
+
+-- AddForeignKey
+ALTER TABLE "AccessCode" ADD CONSTRAINT "AccessCode_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "OAuthClient"("clientId") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AccessCode" ADD CONSTRAINT "AccessCode_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AccessCode" ADD CONSTRAINT "AccessCode_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 9333fb60a4..93806d7e15 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -244,6 +244,7 @@ model User {
hosts Host[]
organizationId Int?
organization Team? @relation("scope", fields: [organizationId], references: [id], onDelete: SetNull)
+ accessCodes AccessCode[]
// Linking account code for orgs v2
//linkedByUserId Int?
//linkedBy User? @relation("linked_account", fields: [linkedByUserId], references: [id], onDelete: Cascade)
@@ -293,6 +294,7 @@ model Team {
routingForms App_RoutingForms_Form[]
apiKeys ApiKey[]
credentials Credential[]
+ accessCodes AccessCode[]
@@unique([slug, parentId])
}
@@ -908,6 +910,33 @@ model SelectedSlots {
@@unique(fields: [userId, slotUtcStartDate, slotUtcEndDate, uid], name: "selectedSlotUnique")
}
+model OAuthClient {
+ clientId String @id @unique
+ redirectUri String
+ clientSecret String
+ name String
+ logo String?
+ accessCodes AccessCode[]
+}
+
+model AccessCode {
+ id Int @id @default(autoincrement())
+ code String
+ clientId String?
+ client OAuthClient? @relation(fields: [clientId], references: [clientId], onDelete: Cascade)
+ expiresAt DateTime
+ scopes AccessScope[]
+ userId Int?
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
+ teamId Int?
+ team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
+}
+
+enum AccessScope {
+ READ_BOOKING
+ READ_PROFILE
+}
+
view BookingTimeStatus {
id Int @unique
uid String?
diff --git a/packages/trpc/react/trpc.ts b/packages/trpc/react/trpc.ts
index fbed7e030c..fc00683e29 100644
--- a/packages/trpc/react/trpc.ts
+++ b/packages/trpc/react/trpc.ts
@@ -41,6 +41,7 @@ const ENDPOINTS = [
"workflows",
"appsRouter",
"googleWorkspace",
+ "oAuth",
] as const;
export type Endpoint = (typeof ENDPOINTS)[number];
diff --git a/packages/trpc/server/routers/viewer/_router.tsx b/packages/trpc/server/routers/viewer/_router.tsx
index be396a1b35..62abc6cbed 100644
--- a/packages/trpc/server/routers/viewer/_router.tsx
+++ b/packages/trpc/server/routers/viewer/_router.tsx
@@ -16,6 +16,7 @@ import { bookingsRouter } from "./bookings/_router";
import { deploymentSetupRouter } from "./deploymentSetup/_router";
import { eventTypesRouter } from "./eventTypes/_router";
import { googleWorkspaceRouter } from "./googleWorkspace/_router";
+import { oAuthRouter } from "./oAuth/_router";
import { viewerOrganizationsRouter } from "./organizations/_router";
import { paymentsRouter } from "./payments/_router";
import { slotsRouter } from "./slots/_router";
@@ -50,6 +51,7 @@ export const viewerRouter = mergeRouters(
features: featureFlagRouter,
appsRouter,
users: userAdminRouter,
+ oAuth: oAuthRouter,
googleWorkspace: googleWorkspaceRouter,
admin: adminRouter,
})
diff --git a/packages/trpc/server/routers/viewer/oAuth/_router.tsx b/packages/trpc/server/routers/viewer/oAuth/_router.tsx
new file mode 100644
index 0000000000..24a5fb0be7
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/oAuth/_router.tsx
@@ -0,0 +1,68 @@
+import authedProcedure, { authedAdminProcedure } from "@calcom/trpc/server/procedures/authedProcedure";
+
+import { router } from "../../../trpc";
+import { ZAddClientInputSchema } from "./addClient.schema";
+import { ZGenerateAuthCodeInputSchema } from "./generateAuthCode.schema";
+import { ZGetClientInputSchema } from "./getClient.schema";
+
+type OAuthRouterHandlerCache = {
+ getClient?: typeof import("./getClient.handler").getClientHandler;
+ addClient?: typeof import("./addClient.handler").addClientHandler;
+ generateAuthCode?: typeof import("./generateAuthCode.handler").generateAuthCodeHandler;
+};
+
+const UNSTABLE_HANDLER_CACHE: OAuthRouterHandlerCache = {};
+
+export const oAuthRouter = router({
+ getClient: authedProcedure.input(ZGetClientInputSchema).query(async ({ ctx, input }) => {
+ if (!UNSTABLE_HANDLER_CACHE.getClient) {
+ UNSTABLE_HANDLER_CACHE.getClient = await import("./getClient.handler").then(
+ (mod) => mod.getClientHandler
+ );
+ }
+
+ // Unreachable code but required for type safety
+ if (!UNSTABLE_HANDLER_CACHE.getClient) {
+ throw new Error("Failed to load handler");
+ }
+
+ return UNSTABLE_HANDLER_CACHE.getClient({
+ input,
+ });
+ }),
+
+ addClient: authedAdminProcedure.input(ZAddClientInputSchema).mutation(async ({ input }) => {
+ if (!UNSTABLE_HANDLER_CACHE.addClient) {
+ UNSTABLE_HANDLER_CACHE.addClient = await import("./addClient.handler").then(
+ (mod) => mod.addClientHandler
+ );
+ }
+
+ // Unreachable code but required for type safety
+ if (!UNSTABLE_HANDLER_CACHE.addClient) {
+ throw new Error("Failed to load handler");
+ }
+
+ return UNSTABLE_HANDLER_CACHE.addClient({
+ input,
+ });
+ }),
+
+ generateAuthCode: authedProcedure.input(ZGenerateAuthCodeInputSchema).mutation(async ({ ctx, input }) => {
+ if (!UNSTABLE_HANDLER_CACHE.generateAuthCode) {
+ UNSTABLE_HANDLER_CACHE.generateAuthCode = await import("./generateAuthCode.handler").then(
+ (mod) => mod.generateAuthCodeHandler
+ );
+ }
+
+ // Unreachable code but required for type safety
+ if (!UNSTABLE_HANDLER_CACHE.generateAuthCode) {
+ throw new Error("Failed to load handler");
+ }
+
+ return UNSTABLE_HANDLER_CACHE.generateAuthCode({
+ ctx,
+ input,
+ });
+ }),
+});
diff --git a/packages/trpc/server/routers/viewer/oAuth/addClient.handler.ts b/packages/trpc/server/routers/viewer/oAuth/addClient.handler.ts
new file mode 100644
index 0000000000..6a1012e9ca
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/oAuth/addClient.handler.ts
@@ -0,0 +1,38 @@
+import { randomBytes, createHash } from "crypto";
+
+import { prisma } from "@calcom/prisma";
+
+import type { TAddClientInputSchema } from "./addClient.schema";
+
+type AddClientOptions = {
+ input: TAddClientInputSchema;
+};
+
+export const addClientHandler = async ({ input }: AddClientOptions) => {
+ const { name, redirectUri, logo } = input;
+
+ const [hashedSecret, secret] = generateSecret();
+ const clientId = randomBytes(32).toString("hex");
+
+ const client = await prisma.oAuthClient.create({
+ data: {
+ name,
+ redirectUri,
+ clientId,
+ clientSecret: hashedSecret,
+ logo,
+ },
+ });
+
+ const clientWithSecret = {
+ ...client,
+ clientSecret: secret,
+ };
+
+ return clientWithSecret;
+};
+
+const hashSecretKey = (apiKey: string): string => createHash("sha256").update(apiKey).digest("hex");
+
+// Generate a random secret
+export const generateSecret = (secret = randomBytes(32).toString("hex")) => [hashSecretKey(secret), secret];
diff --git a/packages/trpc/server/routers/viewer/oAuth/addClient.schema.ts b/packages/trpc/server/routers/viewer/oAuth/addClient.schema.ts
new file mode 100644
index 0000000000..1d2f5ffbb6
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/oAuth/addClient.schema.ts
@@ -0,0 +1,9 @@
+import { z } from "zod";
+
+export const ZAddClientInputSchema = z.object({
+ name: z.string(),
+ redirectUri: z.string(),
+ logo: z.string(),
+});
+
+export type TAddClientInputSchema = z.infer;
diff --git a/packages/trpc/server/routers/viewer/oAuth/generateAuthCode.handler.ts b/packages/trpc/server/routers/viewer/oAuth/generateAuthCode.handler.ts
new file mode 100644
index 0000000000..98c3323c98
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/oAuth/generateAuthCode.handler.ts
@@ -0,0 +1,78 @@
+import { randomBytes } from "crypto";
+
+import dayjs from "@calcom/dayjs";
+import { prisma } from "@calcom/prisma";
+import type { AccessScope } from "@calcom/prisma/enums";
+import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
+
+import { TRPCError } from "@trpc/server";
+
+import type { TGenerateAuthCodeInputSchema } from "./generateAuthCode.schema";
+
+type AddClientOptions = {
+ ctx: {
+ user: NonNullable;
+ };
+ input: TGenerateAuthCodeInputSchema;
+};
+
+export const generateAuthCodeHandler = async ({ ctx, input }: AddClientOptions) => {
+ const { clientId, scopes, teamSlug } = input;
+ const client = await prisma.oAuthClient.findFirst({
+ where: {
+ clientId,
+ },
+ select: {
+ clientId: true,
+ redirectUri: true,
+ name: true,
+ },
+ });
+
+ if (!client) {
+ throw new TRPCError({ code: "UNAUTHORIZED", message: "Client ID not valid" });
+ }
+ const authorizationCode = generateAuthorizationCode();
+
+ const team = teamSlug
+ ? await prisma.team.findFirst({
+ where: {
+ slug: teamSlug,
+ members: {
+ some: {
+ userId: ctx.user.id,
+ role: {
+ in: ["OWNER", "ADMIN"],
+ },
+ },
+ },
+ },
+ })
+ : undefined;
+
+ if (teamSlug && !team) {
+ throw new TRPCError({ code: "UNAUTHORIZED" });
+ }
+
+ await prisma.accessCode.create({
+ data: {
+ code: authorizationCode,
+ clientId,
+ userId: !teamSlug ? ctx.user.id : undefined,
+ teamId: team ? team.id : undefined,
+ expiresAt: dayjs().add(10, "minutes").toDate(),
+ scopes: scopes as [AccessScope],
+ },
+ });
+ return { client, authorizationCode };
+};
+
+function generateAuthorizationCode() {
+ const randomBytesValue = randomBytes(40);
+ const authorizationCode = randomBytesValue
+ .toString("base64")
+ .replace(/=/g, "")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_");
+ return authorizationCode;
+}
diff --git a/packages/trpc/server/routers/viewer/oAuth/generateAuthCode.schema.ts b/packages/trpc/server/routers/viewer/oAuth/generateAuthCode.schema.ts
new file mode 100644
index 0000000000..8b1e267c50
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/oAuth/generateAuthCode.schema.ts
@@ -0,0 +1,9 @@
+import { z } from "zod";
+
+export const ZGenerateAuthCodeInputSchema = z.object({
+ clientId: z.string(),
+ scopes: z.array(z.string()),
+ teamSlug: z.string().optional(),
+});
+
+export type TGenerateAuthCodeInputSchema = z.infer;
diff --git a/packages/trpc/server/routers/viewer/oAuth/getClient.handler.ts b/packages/trpc/server/routers/viewer/oAuth/getClient.handler.ts
new file mode 100644
index 0000000000..2eb53e446a
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/oAuth/getClient.handler.ts
@@ -0,0 +1,24 @@
+import { prisma } from "@calcom/prisma";
+
+import type { TGetClientInputSchema } from "./getClient.schema";
+
+type GetClientOptions = {
+ input: TGetClientInputSchema;
+};
+
+export const getClientHandler = async ({ input }: GetClientOptions) => {
+ const { clientId } = input;
+
+ const client = await prisma.oAuthClient.findFirst({
+ where: {
+ clientId,
+ },
+ select: {
+ clientId: true,
+ redirectUri: true,
+ name: true,
+ logo: true,
+ },
+ });
+ return client;
+};
diff --git a/packages/trpc/server/routers/viewer/oAuth/getClient.schema.ts b/packages/trpc/server/routers/viewer/oAuth/getClient.schema.ts
new file mode 100644
index 0000000000..b068d10ce9
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/oAuth/getClient.schema.ts
@@ -0,0 +1,7 @@
+import { z } from "zod";
+
+export const ZGetClientInputSchema = z.object({
+ clientId: z.string(),
+});
+
+export type TGetClientInputSchema = z.infer;
diff --git a/yarn.lock b/yarn.lock
index c76e9a427b..81feb7176d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -45,6 +45,13 @@ __metadata:
languageName: node
linkType: hard
+"@aashutoshrathi/word-wrap@npm:^1.2.3":
+ version: 1.2.6
+ resolution: "@aashutoshrathi/word-wrap@npm:1.2.6"
+ checksum: ada901b9e7c680d190f1d012c84217ce0063d8f5c5a7725bb91ec3c5ed99bb7572680eb2d2938a531ccbaec39a95422fcd8a6b4a13110c7d98dd75402f66a0cd
+ languageName: node
+ linkType: hard
+
"@achrinza/event-pubsub@npm:5.0.8":
version: 5.0.8
resolution: "@achrinza/event-pubsub@npm:5.0.8"
@@ -91,6 +98,13 @@ __metadata:
languageName: node
linkType: hard
+"@alloc/quick-lru@npm:^5.2.0":
+ version: 5.2.0
+ resolution: "@alloc/quick-lru@npm:5.2.0"
+ checksum: bdc35758b552bcf045733ac047fb7f9a07c4678b944c641adfbd41f798b4b91fffd0fdc0df2578d9b0afc7b4d636aa6e110ead5d6281a2adc1ab90efd7f057f8
+ languageName: node
+ linkType: hard
+
"@ampproject/remapping@npm:^2.2.0":
version: 2.2.1
resolution: "@ampproject/remapping@npm:2.2.1"
@@ -197,25 +211,6 @@ __metadata:
languageName: node
linkType: hard
-"@auth/core@npm:^0.1.4":
- version: 0.1.4
- resolution: "@auth/core@npm:0.1.4"
- dependencies:
- "@panva/hkdf": 1.0.2
- cookie: 0.5.0
- jose: 4.11.1
- oauth4webapi: 2.0.5
- preact: 10.11.3
- preact-render-to-string: 5.2.3
- peerDependencies:
- nodemailer: 6.8.0
- peerDependenciesMeta:
- nodemailer:
- optional: true
- checksum: 64854404ea1883e0deb5535b34bed95cd43fc85094aeaf4f15a79e14045020eb944f844defe857edfc8528a0a024be89cbb2a3069dedef0e9217a74ca6c3eb79
- languageName: node
- linkType: hard
-
"@aws-crypto/ie11-detection@npm:^3.0.0":
version: 3.0.0
resolution: "@aws-crypto/ie11-detection@npm:3.0.0"
@@ -3168,7 +3163,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/runtime@npm:^7.21.0":
+"@babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.21.0":
version: 7.23.1
resolution: "@babel/runtime@npm:7.23.1"
dependencies:
@@ -3498,41 +3493,6 @@ __metadata:
languageName: unknown
linkType: soft
-"@calcom/auth@workspace:apps/auth":
- version: 0.0.0-use.local
- resolution: "@calcom/auth@workspace:apps/auth"
- dependencies:
- "@auth/core": ^0.1.4
- "@calcom/app-store": "*"
- "@calcom/app-store-cli": "*"
- "@calcom/config": "*"
- "@calcom/core": "*"
- "@calcom/dayjs": "*"
- "@calcom/embed-core": "workspace:*"
- "@calcom/embed-react": "workspace:*"
- "@calcom/embed-snippet": "workspace:*"
- "@calcom/features": "*"
- "@calcom/lib": "*"
- "@calcom/prisma": "*"
- "@calcom/trpc": "*"
- "@calcom/tsconfig": "*"
- "@calcom/types": "*"
- "@calcom/ui": "*"
- "@types/node": 16.9.1
- "@types/react": 18.0.26
- "@types/react-dom": ^18.0.9
- eslint: ^8.34.0
- eslint-config-next: ^13.2.1
- next: ^13.4.6
- next-auth: ^4.22.1
- postcss: ^8.4.18
- react: ^18.2.0
- react-dom: ^18.2.0
- tailwindcss: ^3.3.1
- typescript: ^4.9.4
- languageName: unknown
- linkType: soft
-
"@calcom/basecamp3@workspace:packages/app-store/basecamp3":
version: 0.0.0-use.local
resolution: "@calcom/basecamp3@workspace:packages/app-store/basecamp3"
@@ -3622,29 +3582,30 @@ __metadata:
"@calcom/ui": "*"
"@headlessui/react": ^1.5.0
"@heroicons/react": ^1.0.6
- "@prisma/client": ^5.0.0
+ "@prisma/client": ^4.8.1
"@tailwindcss/forms": ^0.5.2
"@types/node": 16.9.1
- "@types/react": 18.0.26
+ "@types/react": ^18.0.17
autoprefixer: ^10.4.12
chart.js: ^3.7.1
client-only: ^0.0.1
- eslint: ^8.34.0
- next: ^13.4.6
- next-auth: ^4.22.1
- next-i18next: ^13.2.2
+ eslint: ^8.22.0
+ next: ^13.1.1
+ next-auth: ^4.10.3
+ next-i18next: ^11.3.0
+ next-transpile-modules: ^10.0.0
postcss: ^8.4.18
- prisma: ^5.0.0
+ prisma: ^4.8.1
prisma-field-encryption: ^1.4.0
react: ^18.2.0
react-chartjs-2: ^4.0.1
react-dom: ^18.2.0
- react-hook-form: ^7.43.3
- react-live-chat-loader: ^2.8.1
+ react-hook-form: ^7.34.2
+ react-live-chat-loader: ^2.7.3
swr: ^1.2.2
- tailwindcss: ^3.3.1
- typescript: ^4.9.4
- zod: ^3.22.2
+ tailwindcss: ^3.2.1
+ typescript: ^4.7.4
+ zod: ^3.20.2
languageName: unknown
linkType: soft
@@ -5517,6 +5478,24 @@ __metadata:
languageName: node
linkType: hard
+"@eslint-community/eslint-utils@npm:^4.2.0":
+ version: 4.4.0
+ resolution: "@eslint-community/eslint-utils@npm:4.4.0"
+ dependencies:
+ eslint-visitor-keys: ^3.3.0
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22
+ languageName: node
+ linkType: hard
+
+"@eslint-community/regexpp@npm:^4.6.1":
+ version: 4.8.1
+ resolution: "@eslint-community/regexpp@npm:4.8.1"
+ checksum: 82d62c845ef42b810f268cfdc84d803a2da01735fb52e902fd34bdc09f92464a094fd8e4802839874b000b2f73f67c972859e813ba705233515d3e954f234bf2
+ languageName: node
+ linkType: hard
+
"@eslint/eslintrc@npm:^1.0.5":
version: 1.3.0
resolution: "@eslint/eslintrc@npm:1.3.0"
@@ -5551,6 +5530,30 @@ __metadata:
languageName: node
linkType: hard
+"@eslint/eslintrc@npm:^2.1.2":
+ version: 2.1.2
+ resolution: "@eslint/eslintrc@npm:2.1.2"
+ dependencies:
+ ajv: ^6.12.4
+ debug: ^4.3.2
+ espree: ^9.6.0
+ globals: ^13.19.0
+ ignore: ^5.2.0
+ import-fresh: ^3.2.1
+ js-yaml: ^4.1.0
+ minimatch: ^3.1.2
+ strip-json-comments: ^3.1.1
+ checksum: bc742a1e3b361f06fedb4afb6bf32cbd27171292ef7924f61c62f2aed73048367bcc7ac68f98c06d4245cd3fabc43270f844e3c1699936d4734b3ac5398814a7
+ languageName: node
+ linkType: hard
+
+"@eslint/js@npm:8.49.0":
+ version: 8.49.0
+ resolution: "@eslint/js@npm:8.49.0"
+ checksum: a6601807c8aeeefe866926ad92ed98007c034a735af20ff709009e39ad1337474243d47908500a3bde04e37bfba16bcf1d3452417f962e1345bc8756edd6b830
+ languageName: node
+ linkType: hard
+
"@ethereumjs/common@npm:^2.5.0, @ethereumjs/common@npm:^2.6.3":
version: 2.6.3
resolution: "@ethereumjs/common@npm:2.6.3"
@@ -6643,6 +6646,17 @@ __metadata:
languageName: node
linkType: hard
+"@humanwhocodes/config-array@npm:^0.11.11":
+ version: 0.11.11
+ resolution: "@humanwhocodes/config-array@npm:0.11.11"
+ dependencies:
+ "@humanwhocodes/object-schema": ^1.2.1
+ debug: ^4.1.1
+ minimatch: ^3.0.5
+ checksum: db84507375ab77b8ffdd24f498a5b49ad6b64391d30dd2ac56885501d03964d29637e05b1ed5aefa09d57ac667e28028bc22d2da872bfcd619652fbdb5f4ca19
+ languageName: node
+ linkType: hard
+
"@humanwhocodes/config-array@npm:^0.11.8":
version: 0.11.8
resolution: "@humanwhocodes/config-array@npm:0.11.8"
@@ -7753,6 +7767,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/env@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/env@npm:13.5.2"
+ checksum: f6ef14b7643049dafc2d53b5091e3f74eed0af14743cfd61f1db7782a99e69b5bef63f36ba700034b23656a264c7ec498aac8fa4f9377dad01e544ffa507388f
+ languageName: node
+ linkType: hard
+
"@next/eslint-plugin-next@npm:13.2.1":
version: 13.2.1
resolution: "@next/eslint-plugin-next@npm:13.2.1"
@@ -7769,6 +7790,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/swc-darwin-arm64@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/swc-darwin-arm64@npm:13.5.2"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@next/swc-darwin-x64@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-darwin-x64@npm:13.4.6"
@@ -7776,6 +7804,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/swc-darwin-x64@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/swc-darwin-x64@npm:13.5.2"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
"@next/swc-linux-arm64-gnu@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-arm64-gnu@npm:13.4.6"
@@ -7783,6 +7818,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/swc-linux-arm64-gnu@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/swc-linux-arm64-gnu@npm:13.5.2"
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@next/swc-linux-arm64-musl@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-arm64-musl@npm:13.4.6"
@@ -7790,6 +7832,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/swc-linux-arm64-musl@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/swc-linux-arm64-musl@npm:13.5.2"
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@next/swc-linux-x64-gnu@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-x64-gnu@npm:13.4.6"
@@ -7797,6 +7846,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/swc-linux-x64-gnu@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/swc-linux-x64-gnu@npm:13.5.2"
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@next/swc-linux-x64-musl@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-x64-musl@npm:13.4.6"
@@ -7804,6 +7860,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/swc-linux-x64-musl@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/swc-linux-x64-musl@npm:13.5.2"
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@next/swc-win32-arm64-msvc@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-win32-arm64-msvc@npm:13.4.6"
@@ -7811,6 +7874,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/swc-win32-arm64-msvc@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/swc-win32-arm64-msvc@npm:13.5.2"
+ conditions: os=win32 & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@next/swc-win32-ia32-msvc@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-win32-ia32-msvc@npm:13.4.6"
@@ -7818,6 +7888,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/swc-win32-ia32-msvc@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/swc-win32-ia32-msvc@npm:13.5.2"
+ conditions: os=win32 & cpu=ia32
+ languageName: node
+ linkType: hard
+
"@next/swc-win32-x64-msvc@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-win32-x64-msvc@npm:13.4.6"
@@ -7825,6 +7902,13 @@ __metadata:
languageName: node
linkType: hard
+"@next/swc-win32-x64-msvc@npm:13.5.2":
+ version: 13.5.2
+ resolution: "@next/swc-win32-x64-msvc@npm:13.5.2"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
"@noble/curves@npm:1.1.0, @noble/curves@npm:~1.1.0":
version: 1.1.0
resolution: "@noble/curves@npm:1.1.0"
@@ -7993,13 +8077,6 @@ __metadata:
languageName: node
linkType: hard
-"@panva/hkdf@npm:1.0.2":
- version: 1.0.2
- resolution: "@panva/hkdf@npm:1.0.2"
- checksum: 75183b4d5ea816ef516dcea70985c610683579a9e2ac540c2d59b9a3ed27eedaff830a43a1c43c1683556a457c92ac66e09109ee995ab173090e4042c4c4bb03
- languageName: node
- linkType: hard
-
"@panva/hkdf@npm:^1.0.2":
version: 1.0.4
resolution: "@panva/hkdf@npm:1.0.4"
@@ -8116,17 +8193,17 @@ __metadata:
languageName: node
linkType: hard
-"@prisma/client@npm:^5.0.0":
- version: 5.3.1
- resolution: "@prisma/client@npm:5.3.1"
+"@prisma/client@npm:^4.8.1":
+ version: 4.16.2
+ resolution: "@prisma/client@npm:4.16.2"
dependencies:
- "@prisma/engines-version": 5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59
+ "@prisma/engines-version": 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81
peerDependencies:
prisma: "*"
peerDependenciesMeta:
prisma:
optional: true
- checksum: 8017b721a231ab7b2b0f932507b02bf075aae2c6f6e630a69d7089ff33bab44fa50b50dd8a81655b7202092ffc19717d484ae5f183fc4f2a1822b0d228991a7c
+ checksum: 38e1356644a764946c69c8691ea4bbed0ba37739d833a435625bd5435912bed4b9bdd7c384125f3a4ab8128faf566027985c0f0840a42741c338d72e40b5d565
languageName: node
linkType: hard
@@ -8177,6 +8254,13 @@ __metadata:
languageName: node
linkType: hard
+"@prisma/engines-version@npm:4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81":
+ version: 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81
+ resolution: "@prisma/engines-version@npm:4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81"
+ checksum: b42c6abe7c1928e546f15449e40ffa455701ef2ab1f62973628ecb4e19ff3652e34609a0d83196d1cbd0864adb44c55e082beec852b11929acf1c15fb57ca45a
+ languageName: node
+ linkType: hard
+
"@prisma/engines-version@npm:5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f":
version: 5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f
resolution: "@prisma/engines-version@npm:5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f"
@@ -8184,10 +8268,10 @@ __metadata:
languageName: node
linkType: hard
-"@prisma/engines-version@npm:5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59":
- version: 5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59
- resolution: "@prisma/engines-version@npm:5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59"
- checksum: c1adf540c9330a54a000c3005a4621c5d8355f2e3b159121587d33f82bf5992f567ac453f0ce76dd48fac427ec4dbc55942228fcee10d522b0d2c03bddbe422a
+"@prisma/engines@npm:4.16.2":
+ version: 4.16.2
+ resolution: "@prisma/engines@npm:4.16.2"
+ checksum: f423e6092c3e558cd089a68ae87459fba7fd390c433df087342b3269c3b04163965b50845150dfe47d01f811781bfff89d5ae81c95ca603c59359ab69ebd810f
languageName: node
linkType: hard
@@ -8198,13 +8282,6 @@ __metadata:
languageName: node
linkType: hard
-"@prisma/engines@npm:5.3.1":
- version: 5.3.1
- resolution: "@prisma/engines@npm:5.3.1"
- checksum: a231adad60ac42569b560ea9782bc181818d8ad15e65283d1317bda5d7aa754e5b536a3f9365ce1eda8961e1eff4eca5978c456fa9764a867fe4339d123e7371
- languageName: node
- linkType: hard
-
"@prisma/extension-accelerate@npm:^0.6.2":
version: 0.6.2
resolution: "@prisma/extension-accelerate@npm:0.6.2"
@@ -11888,6 +11965,15 @@ __metadata:
languageName: node
linkType: hard
+"@swc/helpers@npm:0.5.2":
+ version: 0.5.2
+ resolution: "@swc/helpers@npm:0.5.2"
+ dependencies:
+ tslib: ^2.4.0
+ checksum: 51d7e3d8bd56818c49d6bfbd715f0dbeedc13cf723af41166e45c03e37f109336bbcb57a1f2020f4015957721aeb21e1a7fff281233d797ff7d3dd1f447fa258
+ languageName: node
+ linkType: hard
+
"@szmarczak/http-timer@npm:^4.0.5":
version: 4.0.6
resolution: "@szmarczak/http-timer@npm:4.0.6"
@@ -12779,6 +12865,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/jsonwebtoken@npm:^9.0.3":
+ version: 9.0.3
+ resolution: "@types/jsonwebtoken@npm:9.0.3"
+ dependencies:
+ "@types/node": "*"
+ checksum: 2debf3adb19b827a023205234ec439b7258aee6ca9273472abe360738a84f08db78c6e853172e842ec303169ec0bb2df39701ab9a13b9e7868fe284ef9136567
+ languageName: node
+ linkType: hard
+
"@types/keyv@npm:*, @types/keyv@npm:^3.1.1, @types/keyv@npm:^3.1.4":
version: 3.1.4
resolution: "@types/keyv@npm:3.1.4"
@@ -16430,6 +16525,7 @@ __metadata:
"@playwright/test": ^1.31.2
"@snaplet/copycat": ^0.3.0
"@testing-library/jest-dom": ^5.16.5
+ "@types/jsonwebtoken": ^9.0.3
c8: ^7.13.0
city-timezones: ^1.2.1
dotenv-checker: ^1.1.5
@@ -20183,6 +20279,16 @@ __metadata:
languageName: node
linkType: hard
+"eslint-scope@npm:^7.2.2":
+ version: 7.2.2
+ resolution: "eslint-scope@npm:7.2.2"
+ dependencies:
+ esrecurse: ^4.3.0
+ estraverse: ^5.2.0
+ checksum: ec97dbf5fb04b94e8f4c5a91a7f0a6dd3c55e46bfc7bbcd0e3138c3a76977570e02ed89a1810c778dcd72072ff0e9621ba1379b4babe53921d71e2e4486fda3e
+ languageName: node
+ linkType: hard
+
"eslint-utils@npm:^3.0.0":
version: 3.0.0
resolution: "eslint-utils@npm:3.0.0"
@@ -20208,6 +20314,13 @@ __metadata:
languageName: node
linkType: hard
+"eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3":
+ version: 3.4.3
+ resolution: "eslint-visitor-keys@npm:3.4.3"
+ checksum: 36e9ef87fca698b6fd7ca5ca35d7b2b6eeaaf106572e2f7fd31c12d3bfdaccdb587bba6d3621067e5aece31c8c3a348b93922ab8f7b2cbc6aaab5e1d89040c60
+ languageName: node
+ linkType: hard
+
"eslint@npm:8.4.1":
version: 8.4.1
resolution: "eslint@npm:8.4.1"
@@ -20256,6 +20369,53 @@ __metadata:
languageName: node
linkType: hard
+"eslint@npm:^8.22.0":
+ version: 8.49.0
+ resolution: "eslint@npm:8.49.0"
+ dependencies:
+ "@eslint-community/eslint-utils": ^4.2.0
+ "@eslint-community/regexpp": ^4.6.1
+ "@eslint/eslintrc": ^2.1.2
+ "@eslint/js": 8.49.0
+ "@humanwhocodes/config-array": ^0.11.11
+ "@humanwhocodes/module-importer": ^1.0.1
+ "@nodelib/fs.walk": ^1.2.8
+ ajv: ^6.12.4
+ chalk: ^4.0.0
+ cross-spawn: ^7.0.2
+ debug: ^4.3.2
+ doctrine: ^3.0.0
+ escape-string-regexp: ^4.0.0
+ eslint-scope: ^7.2.2
+ eslint-visitor-keys: ^3.4.3
+ espree: ^9.6.1
+ esquery: ^1.4.2
+ esutils: ^2.0.2
+ fast-deep-equal: ^3.1.3
+ file-entry-cache: ^6.0.1
+ find-up: ^5.0.0
+ glob-parent: ^6.0.2
+ globals: ^13.19.0
+ graphemer: ^1.4.0
+ ignore: ^5.2.0
+ imurmurhash: ^0.1.4
+ is-glob: ^4.0.0
+ is-path-inside: ^3.0.3
+ js-yaml: ^4.1.0
+ json-stable-stringify-without-jsonify: ^1.0.1
+ levn: ^0.4.1
+ lodash.merge: ^4.6.2
+ minimatch: ^3.1.2
+ natural-compare: ^1.4.0
+ optionator: ^0.9.3
+ strip-ansi: ^6.0.1
+ text-table: ^0.2.0
+ bin:
+ eslint: bin/eslint.js
+ checksum: 4dfe257e1e42da2f9da872b05aaaf99b0f5aa022c1a91eee8f2af1ab72651b596366320c575ccd4e0469f7b4c97aff5bb85ae3323ebd6a293c3faef4028b0d81
+ languageName: node
+ linkType: hard
+
"eslint@npm:^8.34.0":
version: 8.34.0
resolution: "eslint@npm:8.34.0"
@@ -20356,6 +20516,17 @@ __metadata:
languageName: node
linkType: hard
+"espree@npm:^9.6.0, espree@npm:^9.6.1":
+ version: 9.6.1
+ resolution: "espree@npm:9.6.1"
+ dependencies:
+ acorn: ^8.9.0
+ acorn-jsx: ^5.3.2
+ eslint-visitor-keys: ^3.4.1
+ checksum: eb8c149c7a2a77b3f33a5af80c10875c3abd65450f60b8af6db1bfcfa8f101e21c1e56a561c6dc13b848e18148d43469e7cd208506238554fb5395a9ea5a1ab9
+ languageName: node
+ linkType: hard
+
"esprima@npm:^4.0.0, esprima@npm:^4.0.1":
version: 4.0.1
resolution: "esprima@npm:4.0.1"
@@ -20375,6 +20546,15 @@ __metadata:
languageName: node
linkType: hard
+"esquery@npm:^1.4.2":
+ version: 1.5.0
+ resolution: "esquery@npm:1.5.0"
+ dependencies:
+ estraverse: ^5.1.0
+ checksum: aefb0d2596c230118656cd4ec7532d447333a410a48834d80ea648b1e7b5c9bc9ed8b5e33a89cb04e487b60d622f44cf5713bf4abed7c97343edefdc84a35900
+ languageName: node
+ linkType: hard
+
"esrecurse@npm:^4.1.0, esrecurse@npm:^4.3.0":
version: 4.3.0
resolution: "esrecurse@npm:4.3.0"
@@ -22549,6 +22729,13 @@ __metadata:
languageName: node
linkType: hard
+"graphemer@npm:^1.4.0":
+ version: 1.4.0
+ resolution: "graphemer@npm:1.4.0"
+ checksum: bab8f0be9b568857c7bec9fda95a89f87b783546d02951c40c33f84d05bb7da3fd10f863a9beb901463669b6583173a8c8cc6d6b306ea2b9b9d5d3d943c3a673
+ languageName: node
+ linkType: hard
+
"graphql-config@npm:^5.0.2":
version: 5.0.2
resolution: "graphql-config@npm:5.0.2"
@@ -23526,6 +23713,13 @@ __metadata:
languageName: node
linkType: hard
+"i18next-fs-backend@npm:^1.1.4":
+ version: 1.2.0
+ resolution: "i18next-fs-backend@npm:1.2.0"
+ checksum: da74d20f2b007f8e34eaf442fa91ad12aaff3b9891e066c6addd6d111b37e370c62370dfbc656730ab2f8afd988f2e7ea1c48301ebb19ccb716fb5965600eddf
+ languageName: node
+ linkType: hard
+
"i18next-fs-backend@npm:^2.1.1":
version: 2.1.3
resolution: "i18next-fs-backend@npm:2.1.3"
@@ -23533,6 +23727,15 @@ __metadata:
languageName: node
linkType: hard
+"i18next@npm:^21.8.13":
+ version: 21.10.0
+ resolution: "i18next@npm:21.10.0"
+ dependencies:
+ "@babel/runtime": ^7.17.2
+ checksum: f997985e2d4d15a62a0936a82ff6420b97f3f971e776fe685bdd50b4de0cb4dc2198bc75efe6b152844794ebd5040d8060d6d152506a687affad534834836d81
+ languageName: node
+ linkType: hard
+
"i18next@npm:^23.2.3":
version: 23.2.3
resolution: "i18next@npm:23.2.3"
@@ -24189,6 +24392,15 @@ __metadata:
languageName: node
linkType: hard
+"is-core-module@npm:^2.13.0":
+ version: 2.13.0
+ resolution: "is-core-module@npm:2.13.0"
+ dependencies:
+ has: ^1.0.3
+ checksum: 053ab101fb390bfeb2333360fd131387bed54e476b26860dc7f5a700bbf34a0ec4454f7c8c4d43e8a0030957e4b3db6e16d35e1890ea6fb654c833095e040355
+ languageName: node
+ linkType: hard
+
"is-core-module@npm:^2.2.0, is-core-module@npm:^2.8.1":
version: 2.8.1
resolution: "is-core-module@npm:2.8.1"
@@ -25157,13 +25369,6 @@ __metadata:
languageName: node
linkType: hard
-"jose@npm:4.11.1":
- version: 4.11.1
- resolution: "jose@npm:4.11.1"
- checksum: cd15cba258d0fd20f6168631ce2e94fda8442df80e43c1033c523915cecdf390a1cc8efe0eab0c2d65935ca973d791c668aea80724d2aa9c2879d4e70f3081d7
- languageName: node
- linkType: hard
-
"jose@npm:4.12.0":
version: 4.12.0
resolution: "jose@npm:4.12.0"
@@ -26299,6 +26504,13 @@ __metadata:
languageName: node
linkType: hard
+"lilconfig@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "lilconfig@npm:2.1.0"
+ checksum: 8549bb352b8192375fed4a74694cd61ad293904eee33f9d4866c2192865c44c4eb35d10782966242634e0cbc1e91fe62b1247f148dc5514918e3a966da7ea117
+ languageName: node
+ linkType: hard
+
"limiter@npm:^1.1.5":
version: 1.1.5
resolution: "limiter@npm:1.1.5"
@@ -28676,6 +28888,31 @@ __metadata:
languageName: node
linkType: hard
+"next-auth@npm:^4.10.3":
+ version: 4.23.1
+ resolution: "next-auth@npm:4.23.1"
+ dependencies:
+ "@babel/runtime": ^7.20.13
+ "@panva/hkdf": ^1.0.2
+ cookie: ^0.5.0
+ jose: ^4.11.4
+ oauth: ^0.9.15
+ openid-client: ^5.4.0
+ preact: ^10.6.3
+ preact-render-to-string: ^5.1.19
+ uuid: ^8.3.2
+ peerDependencies:
+ next: ^12.2.5 || ^13
+ nodemailer: ^6.6.5
+ react: ^17.0.2 || ^18
+ react-dom: ^17.0.2 || ^18
+ peerDependenciesMeta:
+ nodemailer:
+ optional: true
+ checksum: 995114797c257ccf71a82d19fb6316fb7709b552aaaf66444591c505a4b8e00b0cae3f4db4316b63a8cc439076044cc391ab171c4f6ee2e086709c5b3bbfed24
+ languageName: node
+ linkType: hard
+
"next-auth@npm:^4.22.1":
version: 4.22.1
resolution: "next-auth@npm:4.22.1"
@@ -28725,6 +28962,24 @@ __metadata:
languageName: node
linkType: hard
+"next-i18next@npm:^11.3.0":
+ version: 11.3.0
+ resolution: "next-i18next@npm:11.3.0"
+ dependencies:
+ "@babel/runtime": ^7.18.6
+ "@types/hoist-non-react-statics": ^3.3.1
+ core-js: ^3
+ hoist-non-react-statics: ^3.3.2
+ i18next: ^21.8.13
+ i18next-fs-backend: ^1.1.4
+ react-i18next: ^11.18.0
+ peerDependencies:
+ next: ">= 10.0.0"
+ react: ">= 16.8.0"
+ checksum: fbce97a4fbf9ad846c08652471a833c7f173c3e7ddc7cafa1423625b4a684715bb85f76ae06fe9cbed3e70f12b8e78e2459e5bc1a3c3f5c517743f17648f8939
+ languageName: node
+ linkType: hard
+
"next-i18next@npm:^13.2.2":
version: 13.3.0
resolution: "next-i18next@npm:13.3.0"
@@ -28787,6 +29042,15 @@ __metadata:
languageName: node
linkType: hard
+"next-transpile-modules@npm:^10.0.0":
+ version: 10.0.1
+ resolution: "next-transpile-modules@npm:10.0.1"
+ dependencies:
+ enhanced-resolve: ^5.10.0
+ checksum: 6cfffd165769a04b153e60f49f4f3896d16aa688304616f66c7b94299e2229b04db368217e6a7300d3110fe988a6c6e68025bb2cbda3edf49e9b29109ff671bd
+ languageName: node
+ linkType: hard
+
"next-transpile-modules@npm:^8.0.0":
version: 8.0.0
resolution: "next-transpile-modules@npm:8.0.0"
@@ -28806,6 +29070,62 @@ __metadata:
languageName: node
linkType: hard
+"next@npm:^13.1.1":
+ version: 13.5.2
+ resolution: "next@npm:13.5.2"
+ dependencies:
+ "@next/env": 13.5.2
+ "@next/swc-darwin-arm64": 13.5.2
+ "@next/swc-darwin-x64": 13.5.2
+ "@next/swc-linux-arm64-gnu": 13.5.2
+ "@next/swc-linux-arm64-musl": 13.5.2
+ "@next/swc-linux-x64-gnu": 13.5.2
+ "@next/swc-linux-x64-musl": 13.5.2
+ "@next/swc-win32-arm64-msvc": 13.5.2
+ "@next/swc-win32-ia32-msvc": 13.5.2
+ "@next/swc-win32-x64-msvc": 13.5.2
+ "@swc/helpers": 0.5.2
+ busboy: 1.6.0
+ caniuse-lite: ^1.0.30001406
+ postcss: 8.4.14
+ styled-jsx: 5.1.1
+ watchpack: 2.4.0
+ zod: 3.21.4
+ peerDependencies:
+ "@opentelemetry/api": ^1.1.0
+ react: ^18.2.0
+ react-dom: ^18.2.0
+ sass: ^1.3.0
+ dependenciesMeta:
+ "@next/swc-darwin-arm64":
+ optional: true
+ "@next/swc-darwin-x64":
+ optional: true
+ "@next/swc-linux-arm64-gnu":
+ optional: true
+ "@next/swc-linux-arm64-musl":
+ optional: true
+ "@next/swc-linux-x64-gnu":
+ optional: true
+ "@next/swc-linux-x64-musl":
+ optional: true
+ "@next/swc-win32-arm64-msvc":
+ optional: true
+ "@next/swc-win32-ia32-msvc":
+ optional: true
+ "@next/swc-win32-x64-msvc":
+ optional: true
+ peerDependenciesMeta:
+ "@opentelemetry/api":
+ optional: true
+ sass:
+ optional: true
+ bin:
+ next: dist/bin/next
+ checksum: cc0635ad5aaab9fc1f4315b9506361b1abf1a12146542d6054b9434e2e892e967f19fbabd3f3763ba5e227306aa91627c1d73af089e9b853b84c74e20bb0be00
+ languageName: node
+ linkType: hard
+
"next@npm:^13.4.6":
version: 13.4.6
resolution: "next@npm:13.4.6"
@@ -29277,13 +29597,6 @@ __metadata:
languageName: node
linkType: hard
-"oauth4webapi@npm:2.0.5":
- version: 2.0.5
- resolution: "oauth4webapi@npm:2.0.5"
- checksum: 32d0cb7b1cca42d51dfb88075ca2d69fe33172a807e8ea50e317d17cab3bc80588ab8ebcb7eb4600c371a70af4674595b4b341daf6f3a655f1efa1ab715bb6c9
- languageName: node
- linkType: hard
-
"oauth@npm:^0.9.15":
version: 0.9.15
resolution: "oauth@npm:0.9.15"
@@ -29705,6 +30018,20 @@ __metadata:
languageName: node
linkType: hard
+"optionator@npm:^0.9.3":
+ version: 0.9.3
+ resolution: "optionator@npm:0.9.3"
+ dependencies:
+ "@aashutoshrathi/word-wrap": ^1.2.3
+ deep-is: ^0.1.3
+ fast-levenshtein: ^2.0.6
+ levn: ^0.4.1
+ prelude-ls: ^1.2.1
+ type-check: ^0.4.0
+ checksum: 09281999441f2fe9c33a5eeab76700795365a061563d66b098923eb719251a42bdbe432790d35064d0816ead9296dbeb1ad51a733edf4167c96bd5d0882e428a
+ languageName: node
+ linkType: hard
+
"options-defaults@npm:^2.0.39":
version: 2.0.40
resolution: "options-defaults@npm:2.0.40"
@@ -30783,6 +31110,19 @@ __metadata:
languageName: node
linkType: hard
+"postcss-import@npm:^15.1.0":
+ version: 15.1.0
+ resolution: "postcss-import@npm:15.1.0"
+ dependencies:
+ postcss-value-parser: ^4.0.0
+ read-cache: ^1.0.0
+ resolve: ^1.1.7
+ peerDependencies:
+ postcss: ^8.0.0
+ checksum: 7bd04bd8f0235429009d0022cbf00faebc885de1d017f6d12ccb1b021265882efc9302006ba700af6cab24c46bfa2f3bc590be3f9aee89d064944f171b04e2a3
+ languageName: node
+ linkType: hard
+
"postcss-js@npm:^4.0.0":
version: 4.0.0
resolution: "postcss-js@npm:4.0.0"
@@ -30794,6 +31134,17 @@ __metadata:
languageName: node
linkType: hard
+"postcss-js@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "postcss-js@npm:4.0.1"
+ dependencies:
+ camelcase-css: ^2.0.1
+ peerDependencies:
+ postcss: ^8.4.21
+ checksum: 5c1e83efeabeb5a42676193f4357aa9c88f4dc1b3c4a0332c132fe88932b33ea58848186db117cf473049fc233a980356f67db490bd0a7832ccba9d0b3fd3491
+ languageName: node
+ linkType: hard
+
"postcss-load-config@npm:^3.1.4":
version: 3.1.4
resolution: "postcss-load-config@npm:3.1.4"
@@ -30812,6 +31163,24 @@ __metadata:
languageName: node
linkType: hard
+"postcss-load-config@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "postcss-load-config@npm:4.0.1"
+ dependencies:
+ lilconfig: ^2.0.5
+ yaml: ^2.1.1
+ peerDependencies:
+ postcss: ">=8.0.9"
+ ts-node: ">=9.0.0"
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+ ts-node:
+ optional: true
+ checksum: b61f890499ed7dcda1e36c20a9582b17d745bad5e2b2c7bc96942465e406bc43ae03f270c08e60d1e29dab1ee50cb26970b5eb20c9aae30e066e20bd607ae4e4
+ languageName: node
+ linkType: hard
+
"postcss-loader@npm:^4.2.0":
version: 4.3.0
resolution: "postcss-loader@npm:4.3.0"
@@ -30938,6 +31307,17 @@ __metadata:
languageName: node
linkType: hard
+"postcss-nested@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "postcss-nested@npm:6.0.1"
+ dependencies:
+ postcss-selector-parser: ^6.0.11
+ peerDependencies:
+ postcss: ^8.2.14
+ checksum: 7ddb0364cd797de01e38f644879189e0caeb7ea3f78628c933d91cc24f327c56d31269384454fc02ecaf503b44bfa8e08870a7c4cc56b23bc15640e1894523fa
+ languageName: node
+ linkType: hard
+
"postcss-pseudo-companion-classes@npm:^0.1.1":
version: 0.1.1
resolution: "postcss-pseudo-companion-classes@npm:0.1.1"
@@ -31065,17 +31445,6 @@ __metadata:
languageName: node
linkType: hard
-"preact-render-to-string@npm:5.2.3":
- version: 5.2.3
- resolution: "preact-render-to-string@npm:5.2.3"
- dependencies:
- pretty-format: ^3.8.0
- peerDependencies:
- preact: ">=10"
- checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44
- languageName: node
- linkType: hard
-
"preact-render-to-string@npm:^5.1.19":
version: 5.2.6
resolution: "preact-render-to-string@npm:5.2.6"
@@ -31087,7 +31456,7 @@ __metadata:
languageName: node
linkType: hard
-"preact@npm:10.11.3, preact@npm:^10.6.3":
+"preact@npm:^10.6.3":
version: 10.11.3
resolution: "preact@npm:10.11.3"
checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367
@@ -31321,14 +31690,15 @@ __metadata:
languageName: node
linkType: hard
-"prisma@npm:^5.0.0":
- version: 5.3.1
- resolution: "prisma@npm:5.3.1"
+"prisma@npm:^4.8.1":
+ version: 4.16.2
+ resolution: "prisma@npm:4.16.2"
dependencies:
- "@prisma/engines": 5.3.1
+ "@prisma/engines": 4.16.2
bin:
prisma: build/index.js
- checksum: e825adbcb4eec81de276de5507fb7e5486db7788c8c9de36ba6ed73f9e87d9f56b64d0e183a31dc6b80f6737ae1fbcdb110aac44ab89299af646aeb966655bef
+ prisma2: build/index.js
+ checksum: 1d0ed616abd7f8de22441e333b976705f1cb05abcb206965df3fc6a7ea03911ef467dd484a4bc51fdc6cff72dd9857b9852be5f232967a444af0a98c49bfdb76
languageName: node
linkType: hard
@@ -32194,6 +32564,15 @@ __metadata:
languageName: node
linkType: hard
+"react-hook-form@npm:^7.34.2":
+ version: 7.46.2
+ resolution: "react-hook-form@npm:7.46.2"
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18
+ checksum: 5ad2b478337fed3ab1a7bee79844d89a85485626fb1afd4acba54e28adb8f6663d37efefa9000b6ddb33f7de23b765bfac9a1d12e10c63e71463317a307134cf
+ languageName: node
+ linkType: hard
+
"react-hook-form@npm:^7.43.3":
version: 7.43.3
resolution: "react-hook-form@npm:7.43.3"
@@ -32215,6 +32594,24 @@ __metadata:
languageName: node
linkType: hard
+"react-i18next@npm:^11.18.0":
+ version: 11.18.6
+ resolution: "react-i18next@npm:11.18.6"
+ dependencies:
+ "@babel/runtime": ^7.14.5
+ html-parse-stringify: ^3.0.1
+ peerDependencies:
+ i18next: ">= 19.0.0"
+ react: ">= 16.8.0"
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ checksum: 624c0a0313fac4e0d18560b83c99a8bd0a83abc02e5db8d01984e0643ac409d178668aa3a4720d01f7a0d9520d38598dcbff801d6f69a970bae67461de6cd852
+ languageName: node
+ linkType: hard
+
"react-i18next@npm:^12.2.0":
version: 12.3.1
resolution: "react-i18next@npm:12.3.1"
@@ -32338,7 +32735,7 @@ __metadata:
languageName: node
linkType: hard
-"react-live-chat-loader@npm:^2.8.1":
+"react-live-chat-loader@npm:^2.7.3, react-live-chat-loader@npm:^2.8.1":
version: 2.8.1
resolution: "react-live-chat-loader@npm:2.8.1"
peerDependencies:
@@ -33550,6 +33947,19 @@ __metadata:
languageName: node
linkType: hard
+"resolve@npm:^1.22.2":
+ version: 1.22.6
+ resolution: "resolve@npm:1.22.6"
+ dependencies:
+ is-core-module: ^2.13.0
+ path-parse: ^1.0.7
+ supports-preserve-symlinks-flag: ^1.0.0
+ bin:
+ resolve: bin/resolve
+ checksum: d13bf66d4e2ee30d291491f16f2fa44edd4e0cefb85d53249dd6f93e70b2b8c20ec62f01b18662e3cd40e50a7528f18c4087a99490048992a3bb954cf3201a5b
+ languageName: node
+ linkType: hard
+
"resolve@npm:^2.0.0-next.3":
version: 2.0.0-next.3
resolution: "resolve@npm:2.0.0-next.3"
@@ -33586,6 +33996,19 @@ __metadata:
languageName: node
linkType: hard
+"resolve@patch:resolve@^1.22.2#~builtin":
+ version: 1.22.6
+ resolution: "resolve@patch:resolve@npm%3A1.22.6#~builtin::version=1.22.6&hash=c3c19d"
+ dependencies:
+ is-core-module: ^2.13.0
+ path-parse: ^1.0.7
+ supports-preserve-symlinks-flag: ^1.0.0
+ bin:
+ resolve: bin/resolve
+ checksum: 9d3b3c67aefd12cecbe5f10ca4d1f51ea190891096497c43f301b086883b426466918c3a64f1bbf1788fabb52b579d58809614006c5d0b49186702b3b8fb746a
+ languageName: node
+ linkType: hard
+
"resolve@patch:resolve@^2.0.0-next.3#~builtin":
version: 2.0.0-next.3
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.3#~builtin::version=2.0.0-next.3&hash=c3c19d"
@@ -35693,6 +36116,24 @@ __metadata:
languageName: node
linkType: hard
+"sucrase@npm:^3.32.0":
+ version: 3.34.0
+ resolution: "sucrase@npm:3.34.0"
+ dependencies:
+ "@jridgewell/gen-mapping": ^0.3.2
+ commander: ^4.0.0
+ glob: 7.1.6
+ lines-and-columns: ^1.1.6
+ mz: ^2.7.0
+ pirates: ^4.0.1
+ ts-interface-checker: ^0.1.9
+ bin:
+ sucrase: bin/sucrase
+ sucrase-node: bin/sucrase-node
+ checksum: 61860063bdf6103413698e13247a3074d25843e91170825a9752e4af7668ffadd331b6e99e92fc32ee5b3c484ee134936f926fa9039d5711fafff29d017a2110
+ languageName: node
+ linkType: hard
+
"superagent@npm:^5.1.1":
version: 5.3.1
resolution: "superagent@npm:5.3.1"
@@ -36044,6 +36485,39 @@ __metadata:
languageName: node
linkType: hard
+"tailwindcss@npm:^3.2.1":
+ version: 3.3.3
+ resolution: "tailwindcss@npm:3.3.3"
+ dependencies:
+ "@alloc/quick-lru": ^5.2.0
+ arg: ^5.0.2
+ chokidar: ^3.5.3
+ didyoumean: ^1.2.2
+ dlv: ^1.1.3
+ fast-glob: ^3.2.12
+ glob-parent: ^6.0.2
+ is-glob: ^4.0.3
+ jiti: ^1.18.2
+ lilconfig: ^2.1.0
+ micromatch: ^4.0.5
+ normalize-path: ^3.0.0
+ object-hash: ^3.0.0
+ picocolors: ^1.0.0
+ postcss: ^8.4.23
+ postcss-import: ^15.1.0
+ postcss-js: ^4.0.1
+ postcss-load-config: ^4.0.1
+ postcss-nested: ^6.0.1
+ postcss-selector-parser: ^6.0.11
+ resolve: ^1.22.2
+ sucrase: ^3.32.0
+ bin:
+ tailwind: lib/cli.js
+ tailwindcss: lib/cli.js
+ checksum: 0195c7a3ebb0de5e391d2a883d777c78a4749f0c532d204ee8aea9129f2ed8e701d8c0c276aa5f7338d07176a3c2a7682c1d0ab9c8a6c2abe6d9325c2954eb50
+ languageName: node
+ linkType: hard
+
"tailwindcss@npm:^3.3.1":
version: 3.3.1
resolution: "tailwindcss@npm:3.3.1"
@@ -37372,6 +37846,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@npm:^4.7.4":
+ version: 4.9.5
+ resolution: "typescript@npm:4.9.5"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db
+ languageName: node
+ linkType: hard
+
"typescript@npm:^4.9.4":
version: 4.9.4
resolution: "typescript@npm:4.9.4"
@@ -37382,6 +37866,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@patch:typescript@^4.7.4#~builtin":
+ version: 4.9.5
+ resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d
+ languageName: node
+ linkType: hard
+
"typescript@patch:typescript@^4.9.4#~builtin":
version: 4.9.4
resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin::version=4.9.4&hash=23ec76"
@@ -39873,6 +40367,13 @@ __metadata:
languageName: node
linkType: hard
+"yaml@npm:^2.1.1, yaml@npm:^2.3.1":
+ version: 2.3.2
+ resolution: "yaml@npm:2.3.2"
+ checksum: acd80cc24df12c808c6dec8a0176d404ef9e6f08ad8786f746ecc9d8974968c53c6e8a67fdfabcc5f99f3dc59b6bb0994b95646ff03d18e9b1dcd59eccc02146
+ languageName: node
+ linkType: hard
+
"yaml@npm:^2.2.1":
version: 2.3.1
resolution: "yaml@npm:2.3.1"
@@ -39880,13 +40381,6 @@ __metadata:
languageName: node
linkType: hard
-"yaml@npm:^2.3.1":
- version: 2.3.2
- resolution: "yaml@npm:2.3.2"
- checksum: acd80cc24df12c808c6dec8a0176d404ef9e6f08ad8786f746ecc9d8974968c53c6e8a67fdfabcc5f99f3dc59b6bb0994b95646ff03d18e9b1dcd59eccc02146
- languageName: node
- linkType: hard
-
"yargs-parser@npm:^18.1.2, yargs-parser@npm:^18.1.3":
version: 18.1.3
resolution: "yargs-parser@npm:18.1.3"
@@ -40109,7 +40603,7 @@ __metadata:
languageName: node
linkType: hard
-"zod@npm:^3.21.4, zod@npm:^3.22.2":
+"zod@npm:^3.20.2, zod@npm:^3.21.4, zod@npm:^3.22.2":
version: 3.22.2
resolution: "zod@npm:3.22.2"
checksum: 231e2180c8eabb56e88680d80baff5cf6cbe6d64df3c44c50ebe52f73081ecd0229b1c7215b9552537f537a36d9e36afac2737ddd86dc14e3519bdbc777e82b9