Merge branch 'main' into teste2e-phoneQuestionColletive
This commit is contained in:
commit
c49afdc45b
|
@ -37,7 +37,7 @@ type SignupProps = inferSSRProps<typeof getServerSideProps>;
|
|||
const checkValidEmail = (email: string) => z.string().email().safeParse(email).success;
|
||||
|
||||
const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string) => {
|
||||
const [emailUser, emailDomain] = email.split("@");
|
||||
const [emailUser, emailDomain = ""] = email.split("@");
|
||||
const username =
|
||||
emailDomain === autoAcceptEmailDomain
|
||||
? slugify(emailUser)
|
||||
|
@ -143,7 +143,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoA
|
|||
methods.clearErrors("apiError");
|
||||
}
|
||||
|
||||
if (methods.getValues().username === undefined && isOrgInviteByLink && orgAutoAcceptEmail) {
|
||||
if (!methods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
|
||||
methods.setValue(
|
||||
"username",
|
||||
getOrgUsernameFromEmail(methods.getValues().email, orgAutoAcceptEmail)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
E2E_CLIPBOARD_VALUE?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export type Window = typeof window;
|
||||
// creates the single server fixture
|
||||
export const createClipboardFixture = (page: Page) => {
|
||||
return {
|
||||
reset: async () => {
|
||||
await page.evaluate(() => {
|
||||
delete window.E2E_CLIPBOARD_VALUE;
|
||||
});
|
||||
},
|
||||
get: async () => {
|
||||
return getClipboardValue({ page });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function getClipboardValue({ page }: { page: Page }) {
|
||||
return page.evaluate(() => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
setInterval(() => {
|
||||
if (!window.E2E_CLIPBOARD_VALUE) return;
|
||||
resolve(window.E2E_CLIPBOARD_VALUE);
|
||||
}, 500);
|
||||
setTimeout(() => reject(new Error("Timeout")), 1000);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -86,12 +86,14 @@ const createTeamAndAddUser = async (
|
|||
user,
|
||||
isUnpublished,
|
||||
isOrg,
|
||||
isOrgVerified,
|
||||
hasSubteam,
|
||||
organizationId,
|
||||
}: {
|
||||
user: { id: number; username: string | null; role?: MembershipRole };
|
||||
user: { id: number; email: string; username: string | null; role?: MembershipRole };
|
||||
isUnpublished?: boolean;
|
||||
isOrg?: boolean;
|
||||
isOrgVerified?: boolean;
|
||||
hasSubteam?: true;
|
||||
organizationId?: number | null;
|
||||
},
|
||||
|
@ -103,7 +105,14 @@ const createTeamAndAddUser = async (
|
|||
};
|
||||
data.metadata = {
|
||||
...(isUnpublished ? { requestedSlug: slug } : {}),
|
||||
...(isOrg ? { isOrganization: true } : {}),
|
||||
...(isOrg
|
||||
? {
|
||||
isOrganization: true,
|
||||
isOrganizationVerified: !!isOrgVerified,
|
||||
orgAutoAcceptEmail: user.email.split("@")[1],
|
||||
isOrganizationConfigured: false,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
data.slug = !isUnpublished ? slug : undefined;
|
||||
if (isOrg && hasSubteam) {
|
||||
|
@ -145,6 +154,7 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
|||
teamEventSlug?: string;
|
||||
teamEventLength?: number;
|
||||
isOrg?: boolean;
|
||||
isOrgVerified?: boolean;
|
||||
hasSubteam?: true;
|
||||
isUnpublished?: true;
|
||||
} = {}
|
||||
|
@ -292,9 +302,10 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
|||
if (scenario.hasTeam) {
|
||||
const team = await createTeamAndAddUser(
|
||||
{
|
||||
user: { id: user.id, username: user.username, role: "OWNER" },
|
||||
user: { id: user.id, email: user.email, username: user.username, role: "OWNER" },
|
||||
isUnpublished: scenario.isUnpublished,
|
||||
isOrg: scenario.isOrg,
|
||||
isOrgVerified: scenario.isOrgVerified,
|
||||
hasSubteam: scenario.hasSubteam,
|
||||
organizationId: opts?.organizationId,
|
||||
},
|
||||
|
@ -410,6 +421,9 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
|||
routingForms: user.routingForms,
|
||||
self,
|
||||
apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page),
|
||||
/**
|
||||
* @deprecated use apiLogin instead
|
||||
*/
|
||||
login: async () => login({ ...(await self()), password: user.username }, store.page),
|
||||
logout: async () => {
|
||||
await page.goto("/auth/logout");
|
||||
|
|
|
@ -4,10 +4,12 @@ import type { API } from "mailhog";
|
|||
import mailhog from "mailhog";
|
||||
|
||||
import { IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
||||
import { createBookingsFixture } from "../fixtures/bookings";
|
||||
import { createClipboardFixture } from "../fixtures/clipboard";
|
||||
import { createEmbedsFixture } from "../fixtures/embeds";
|
||||
import { createOrgsFixture } from "../fixtures/orgs";
|
||||
import { createPaymentsFixture } from "../fixtures/payments";
|
||||
|
@ -28,6 +30,7 @@ export interface Fixtures {
|
|||
emails?: API;
|
||||
routingForms: ReturnType<typeof createRoutingFormsFixture>;
|
||||
bookingPage: ReturnType<typeof createBookingPageFixture>;
|
||||
clipboard: ReturnType<typeof createClipboardFixture>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -85,6 +88,8 @@ export const test = base.extend<Fixtures>({
|
|||
const mailhogAPI = mailhog();
|
||||
await use(mailhogAPI);
|
||||
} else {
|
||||
//FIXME: Ideally we should error out here. If someone is running tests with mailhog disabled, they should be aware of it
|
||||
logger.warn("Mailhog is not enabled - Skipping Emails verification");
|
||||
await use(undefined);
|
||||
}
|
||||
},
|
||||
|
@ -92,4 +97,8 @@ export const test = base.extend<Fixtures>({
|
|||
const bookingPage = createBookingPageFixture(page);
|
||||
await use(bookingPage);
|
||||
},
|
||||
clipboard: async ({ page }, use) => {
|
||||
const clipboard = createClipboardFixture(page);
|
||||
await use(clipboard);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import type { Frame, Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { createHash } from "crypto";
|
||||
import EventEmitter from "events";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { createServer } from "http";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import type { API, Messages } from "mailhog";
|
||||
import { totp } from "otplib";
|
||||
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
@ -278,3 +280,12 @@ export async function createUserWithSeatedEventAndAttendees(
|
|||
});
|
||||
return { user, eventType, booking };
|
||||
}
|
||||
|
||||
export function generateTotpCode(email: string) {
|
||||
const secret = createHash("md5")
|
||||
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
|
||||
.digest("hex");
|
||||
|
||||
totp.options = { step: 90 };
|
||||
return totp.generate(secret);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { JSDOM } from "jsdom";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { API, Messages } from "mailhog";
|
||||
|
||||
import { getEmailsReceivedByUser } from "../lib/testUtils";
|
||||
|
||||
export async function expectInvitationEmailToBeReceived(
|
||||
page: Page,
|
||||
emails: API | undefined,
|
||||
userEmail: string,
|
||||
subject: string,
|
||||
returnLink?: string
|
||||
) {
|
||||
if (!emails) return null;
|
||||
// We need to wait for the email to go through, otherwise it will fail
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(5000);
|
||||
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
|
||||
expect(receivedEmails?.total).toBe(1);
|
||||
const [firstReceivedEmail] = (receivedEmails as Messages).items;
|
||||
expect(firstReceivedEmail.subject).toBe(subject);
|
||||
if (!returnLink) return;
|
||||
const dom = new JSDOM(firstReceivedEmail.html);
|
||||
const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`);
|
||||
return anchor?.getAttribute("href");
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import { expect } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { generateTotpCode } from "../lib/testUtils";
|
||||
import { expectInvitationEmailToBeReceived } from "./expects";
|
||||
|
||||
test.afterAll(({ users, emails }) => {
|
||||
users.deleteAll();
|
||||
emails?.deleteAll();
|
||||
});
|
||||
|
||||
function capitalize(text: string) {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
test.describe("Organization", () => {
|
||||
test("should be able to create an organization and complete onboarding", async ({
|
||||
page,
|
||||
users,
|
||||
emails,
|
||||
}) => {
|
||||
const orgOwner = await users.create();
|
||||
const orgDomain = `${orgOwner.username}-org`;
|
||||
const orgName = capitalize(`${orgOwner.username}-org`);
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/new");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("Basic info", async () => {
|
||||
// Check required fields
|
||||
await page.locator("button[type=submit]").click();
|
||||
await expect(page.locator(".text-red-700")).toHaveCount(3);
|
||||
|
||||
// Happy path
|
||||
await page.locator("input[name=adminEmail]").fill(`john@${orgDomain}.com`);
|
||||
expect(await page.locator("input[name=name]").inputValue()).toEqual(orgName);
|
||||
expect(await page.locator("input[name=slug]").inputValue()).toEqual(orgDomain);
|
||||
await page.locator("button[type=submit]").click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check admin email about code verification
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
`john@${orgOwner.username}-org.com`,
|
||||
"Verify your email to create an organization"
|
||||
);
|
||||
|
||||
await test.step("Verification", async () => {
|
||||
// Code verification
|
||||
await expect(page.locator("#modal-title")).toBeVisible();
|
||||
await page.locator("input[name='2fa1']").fill(generateTotpCode(`john@${orgDomain}.com`));
|
||||
await page.locator("button:text('Verify')").click();
|
||||
|
||||
// Check admin email about DNS pending action
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
"admin@example.com",
|
||||
"New organization created: pending action"
|
||||
);
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/set-password");
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Admin password", async () => {
|
||||
// Check required fields
|
||||
await page.locator("button[type=submit]").click();
|
||||
await expect(page.locator(".text-red-700")).toHaveCount(3); // 3 password hints
|
||||
|
||||
// Happy path
|
||||
await page.locator("input[name='password']").fill("ADMIN_user2023$");
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/about");
|
||||
});
|
||||
|
||||
await test.step("About the organization", async () => {
|
||||
// Choosing an avatar
|
||||
await page.locator('button:text("Upload")').click();
|
||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||
await page.getByText("Choose a file...").click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
|
||||
await page.locator('button:text("Save")').click();
|
||||
|
||||
// About text
|
||||
await page.locator('textarea[name="about"]').fill("This is a testing org");
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/onboard-admins");
|
||||
});
|
||||
|
||||
await test.step("On-board administrators", async () => {
|
||||
// Required field
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Happy path
|
||||
await page.locator('textarea[name="emails"]').fill(`rick@${orgDomain}.com`);
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Check if invited admin received the invitation email
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
`rick@${orgDomain}.com`,
|
||||
`${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
|
||||
);
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/add-teams");
|
||||
});
|
||||
|
||||
await test.step("Create teams", async () => {
|
||||
// Initial state
|
||||
await expect(page.locator('input[name="teams.0.name"]')).toHaveCount(1);
|
||||
await expect(page.locator('button:text("Continue")')).toBeDisabled();
|
||||
|
||||
// Filling one team
|
||||
await page.locator('input[name="teams.0.name"]').fill("Marketing");
|
||||
await expect(page.locator('button:text("Continue")')).toBeEnabled();
|
||||
|
||||
// Adding another team
|
||||
await page.locator('button:text("Add a team")').click();
|
||||
await expect(page.locator('button:text("Continue")')).toBeDisabled();
|
||||
await expect(page.locator('input[name="teams.1.name"]')).toHaveCount(1);
|
||||
await page.locator('input[name="teams.1.name"]').fill("Sales");
|
||||
await expect(page.locator('button:text("Continue")')).toBeEnabled();
|
||||
|
||||
// Finishing the creation wizard
|
||||
await page.locator('button:text("Continue")').click();
|
||||
await page.waitForURL("/event-types");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,119 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { expectInvitationEmailToBeReceived } from "./expects";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async ({ users, emails, clipboard }) => {
|
||||
clipboard.reset();
|
||||
await users.deleteAll();
|
||||
emails?.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Organization", () => {
|
||||
test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
|
||||
const { team: org } = await orgOwner.getOrg();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (external user)", async () => {
|
||||
const invitedUserEmail = `rick@domain-${Date.now()}.com`;
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
// Check newly invited member exists and is pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(1);
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!inviteLink) return null;
|
||||
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
const newPage = await context.newPage();
|
||||
newPage.goto(inviteLink);
|
||||
await newPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await expect(newPage.locator(".text-red-700")).toHaveCount(3); // 3 password hints
|
||||
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await newPage.waitForURL("/getting-started?from=signup");
|
||||
await context.close();
|
||||
await newPage.close();
|
||||
|
||||
// Check newly invited member is not pending anymore
|
||||
await page.bringToFront();
|
||||
await page.goto("/settings/organizations/members");
|
||||
page.locator(`[data-testid="login-form"]`);
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
await test.step("To the organization by invite link", async () => {
|
||||
// Get the invite link
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
|
||||
const inviteLink = await clipboard.get();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
const inviteLinkPage = await context.newPage();
|
||||
await inviteLinkPage.goto(inviteLink);
|
||||
await inviteLinkPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await expect(inviteLinkPage.locator(".text-red-700")).toHaveCount(4); // email + 3 password hints
|
||||
|
||||
// Happy path
|
||||
await inviteLinkPage.locator("input[name=email]").fill(`rick@domain-${Date.now()}.com`);
|
||||
await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await inviteLinkPage.waitForURL("/getting-started");
|
||||
});
|
||||
});
|
||||
|
||||
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
|
||||
const { team: org } = await orgOwner.getOrg();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (internal user)", async () => {
|
||||
const invitedUserEmail = `rick@example.com`;
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`
|
||||
);
|
||||
|
||||
// Check newly invited member exists and is pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1098,6 +1098,7 @@
|
|||
"developer_documentation": "Documentation pour développeurs",
|
||||
"get_in_touch": "Contactez-nous",
|
||||
"contact_support": "Contacter l'assistance",
|
||||
"premium_support": "Assistance Premium",
|
||||
"community_support": "Aide communautaire",
|
||||
"feedback": "Commentaires",
|
||||
"submitted_feedback": "Merci pour vos commentaires !",
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"@playwright/test": "^1.31.2",
|
||||
"@snaplet/copycat": "^0.3.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@types/jsdom": "^21.1.3",
|
||||
"@types/jsonwebtoken": "^9.0.3",
|
||||
"c8": "^7.13.0",
|
||||
"checkly": "latest",
|
||||
|
|
|
@ -72,7 +72,10 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
amount: payment.amount,
|
||||
externalId: invoice.paymentRequest,
|
||||
currency: payment.currency,
|
||||
data: Object.assign({}, { invoice }) as unknown as Prisma.InputJsonValue,
|
||||
data: Object.assign(
|
||||
{},
|
||||
{ invoice: { ...invoice, isPaid: await invoice.isPaid() } }
|
||||
) as unknown as Prisma.InputJsonValue,
|
||||
fee: 0,
|
||||
refunded: false,
|
||||
success: false,
|
||||
|
@ -84,7 +87,7 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
}
|
||||
return paymentData;
|
||||
} catch (error) {
|
||||
log.error("Alby: Payment could not be created", bookingId);
|
||||
log.error("Alby: Payment could not be created", bookingId, JSON.stringify(error));
|
||||
throw new Error(ErrorCode.PaymentCreationFailure);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import { google } from "googleapis";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
@ -14,14 +16,13 @@ const scopes = [
|
|||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") {
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Get token from Google Calendar API
|
||||
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||
if (!client_id) throw new HttpError({ statusCode: 400, message: "Google client_id missing." });
|
||||
if (!client_secret) throw new HttpError({ statusCode: 400, message: "Google client_secret missing." });
|
||||
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
|
||||
|
@ -37,5 +38,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
});
|
||||
|
||||
res.status(200).json({ url: authUrl });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
|
|
|
@ -3,6 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { WEBAPP_URL_FOR_OAUTH, CAL_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
@ -12,24 +14,23 @@ import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
|||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { code } = req.query;
|
||||
const state = decodeOAuthState(req);
|
||||
|
||||
if (typeof code !== "string") {
|
||||
res.status(400).json({ message: "`code` must be a string" });
|
||||
return;
|
||||
throw new HttpError({ statusCode: 400, message: "`code` must be a string" });
|
||||
}
|
||||
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||
if (!client_id) throw new HttpError({ statusCode: 400, message: "Google client_id missing." });
|
||||
if (!client_secret) throw new HttpError({ statusCode: 400, message: "Google client_secret missing." });
|
||||
|
||||
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
|
||||
|
||||
|
@ -107,3 +108,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
getInstalledAppPath({ variant: "calendar", slug: "google-calendar" })
|
||||
);
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
|
|
|
@ -11,24 +11,6 @@ const MembersView = () => {
|
|||
<LicenseRequired>
|
||||
<Meta title={t("organization_members")} description={t("organization_description")} />
|
||||
<div>
|
||||
{/* {team && (
|
||||
<>
|
||||
{isInviteOpen && (
|
||||
<TeamInviteList
|
||||
teams={[
|
||||
{
|
||||
id: team.id,
|
||||
accepted: team.membership.accepted || false,
|
||||
logo: team.logo,
|
||||
name: team.name,
|
||||
slug: team.slug,
|
||||
role: team.membership.role,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)} */}
|
||||
<UserListTable />
|
||||
</div>
|
||||
</LicenseRequired>
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
TextAreaField,
|
||||
} from "@calcom/ui";
|
||||
import { Link } from "@calcom/ui/components/icon";
|
||||
import type { Window as WindowWithClipboardValue } from "@calcom/web/playwright/fixtures/clipboard";
|
||||
|
||||
import type { PendingMember } from "../lib/types";
|
||||
import { GoogleWorkspaceInviteButton } from "./GoogleWorkspaceInviteButton";
|
||||
|
@ -92,8 +93,15 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
|
||||
const inviteLink =
|
||||
isOrgInvite || (props?.orgMembers && props.orgMembers?.length > 0) ? orgInviteLink : teamInviteLink;
|
||||
try {
|
||||
await navigator.clipboard.writeText(inviteLink);
|
||||
showToast(t("invite_link_copied"), "success");
|
||||
} catch (e) {
|
||||
if (process.env.NEXT_PUBLIC_IS_E2E) {
|
||||
(window as WindowWithClipboardValue).E2E_CLIPBOARD_VALUE = inviteLink;
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const options: MembershipRoleOption[] = useMemo(() => {
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { Dayjs } from "@calcom/dayjs";
|
|||
import dayjs from "@calcom/dayjs";
|
||||
import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import type { WorkingHours } from "@calcom/types/schedule";
|
||||
import {
|
||||
Dialog,
|
||||
|
@ -210,19 +209,12 @@ const DateOverrideInputDialog = ({
|
|||
onChange: (newValue: TimeRange[]) => void;
|
||||
value?: TimeRange[];
|
||||
}) => {
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const [open, setOpen] = useState(false);
|
||||
{
|
||||
/* enableOverflow is used to allow overflow when there are too many overrides to show on mobile.
|
||||
ref:- https://github.com/calcom/cal.com/pull/6215
|
||||
*/
|
||||
}
|
||||
const enableOverflow = isMobile;
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{Trigger}</DialogTrigger>
|
||||
|
||||
<DialogContent enableOverflow={enableOverflow} size="md" className="p-0">
|
||||
<DialogContent enableOverflow={true} size="md" className="p-0">
|
||||
<DateOverrideForm
|
||||
excludedDates={excludedDates}
|
||||
{...passThroughProps}
|
||||
|
|
|
@ -204,12 +204,15 @@ export function UserListTable() {
|
|||
id: "teams",
|
||||
header: "Teams",
|
||||
cell: ({ row }) => {
|
||||
const { teams, accepted } = row.original;
|
||||
const { teams, accepted, email } = row.original;
|
||||
// TODO: Implement click to filter
|
||||
return (
|
||||
<div className="flex h-full flex-wrap items-center gap-2">
|
||||
{accepted ? null : (
|
||||
<Badge variant="red" className="text-xs">
|
||||
<Badge
|
||||
variant="red"
|
||||
className="text-xs"
|
||||
data-testid={`email-${email.replace("@", "")}-pending`}>
|
||||
Pending
|
||||
</Badge>
|
||||
)}
|
||||
|
|
|
@ -7,12 +7,7 @@ import { sendAdminOrganizationNotification } from "@calcom/emails";
|
|||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
import {
|
||||
IS_TEAM_BILLING_ENABLED,
|
||||
RESERVED_SUBDOMAINS,
|
||||
IS_PRODUCTION,
|
||||
WEBAPP_URL,
|
||||
} from "@calcom/lib/constants";
|
||||
import { IS_TEAM_BILLING_ENABLED, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
@ -175,7 +170,6 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
|||
|
||||
return { user: { ...createOwnerOrg, password } };
|
||||
} else {
|
||||
if (!IS_PRODUCTION) return { checked: true };
|
||||
const language = await getTranslation(input.language ?? "en", "common");
|
||||
|
||||
const secret = createHash("md5")
|
||||
|
|
|
@ -2,6 +2,7 @@ import { createHash } from "crypto";
|
|||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { totpRawCheck } from "@calcom/lib/totp";
|
||||
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
@ -21,7 +22,10 @@ export const verifyCodeHandler = async ({ ctx, input }: VerifyCodeOptions) => {
|
|||
|
||||
if (!user || !email || !code) throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
|
||||
if (!IS_PRODUCTION) return true;
|
||||
if (!IS_PRODUCTION || process.env.NEXT_PUBLIC_IS_E2E) {
|
||||
logger.warn(`Skipping code verification in dev/E2E environment`);
|
||||
return true;
|
||||
}
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: email,
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -13331,6 +13331,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jsdom@npm:^21.1.3":
|
||||
version: 21.1.4
|
||||
resolution: "@types/jsdom@npm:21.1.4"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
"@types/tough-cookie": "*"
|
||||
parse5: ^7.0.0
|
||||
checksum: 915f619111dadd8d1bb7f12b6736c9d2e486911e1aed086de5fb003e7e40ae1e368da322dc04f2122ef47faf40ca75b9315ae2df3e8011f882dcf84660fb0d68
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jsforce@npm:^1.11.0":
|
||||
version: 1.11.0
|
||||
resolution: "@types/jsforce@npm:1.11.0"
|
||||
|
@ -17197,6 +17208,7 @@ __metadata:
|
|||
"@playwright/test": ^1.31.2
|
||||
"@snaplet/copycat": ^0.3.0
|
||||
"@testing-library/jest-dom": ^5.16.5
|
||||
"@types/jsdom": ^21.1.3
|
||||
"@types/jsonwebtoken": ^9.0.3
|
||||
c8: ^7.13.0
|
||||
checkly: latest
|
||||
|
|
Loading…
Reference in New Issue
Block a user