* refactor: apply biome formatting to apps/web (batch 1) Formats apps/web non-modules directories: - apps/web/app - apps/web/lib - apps/web/pages - apps/web/styles - apps/web/server - apps/web/test - Root-level .ts and .mjs files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules (batch 2) Formats smaller apps/web/modules directories: - onboarding, data-table, users, apps, auth - integration-attribute-sync, shell, availability - feature-flags, team, webhooks, getting-started - feature-opt-in, troubleshooter, schedules, notifications - signup-view.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules (batch 3) Formats medium apps/web/modules directories: - bookings - event-types - insights - embed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules/ee (batch 4) Formats apps/web/modules/ee directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web (batch 5) Formats remaining apps/web directories: - apps/web/modules/settings - apps/web/modules/booking-audit - apps/web/playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/components (batch 6) Formats remaining apps/web/components files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to new apps/web files from main Formats newly added/modified files from main merge: - WorkflowStepContainer.tsx (resolved merge conflicts) - Newly moved files (blocklist, form-builder, schedules, etc.) - Other files modified in main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to new apps/web files from main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
225 lines
6.9 KiB
TypeScript
225 lines
6.9 KiB
TypeScript
import type { GetServerSidePropsContext } from "next";
|
|
import { z } from "zod";
|
|
|
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
|
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
|
|
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
|
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
|
import { emailSchema } from "@calcom/lib/emailSchema";
|
|
import slugify from "@calcom/lib/slugify";
|
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
|
|
|
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
|
|
|
const checkValidEmail = (email: string) => emailSchema.safeParse(email).success;
|
|
|
|
const querySchema = z.object({
|
|
username: z
|
|
.string()
|
|
.optional()
|
|
.transform((val) => val || ""),
|
|
email: emailSchema.optional(),
|
|
});
|
|
|
|
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
|
const featuresRepository = new FeaturesRepository(prisma);
|
|
const emailVerificationEnabled =
|
|
await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
|
|
const signupDisabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("disable-signup");
|
|
const onboardingV3Enabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
|
|
|
|
const token = z.string().optional().parse(ctx.query.token);
|
|
const redirectUrlData = z
|
|
.string()
|
|
.refine((value) => value.startsWith(WEBAPP_URL), {
|
|
params: (value: string) => ({ value }),
|
|
message: "Redirect URL must start with 'cal.com'",
|
|
})
|
|
.optional()
|
|
.safeParse(ctx.query.redirect);
|
|
|
|
const redirectUrl = redirectUrlData.success && redirectUrlData.data ? redirectUrlData.data : null;
|
|
|
|
const session = await getServerSession({
|
|
req: ctx.req,
|
|
});
|
|
|
|
if (session?.user?.id) {
|
|
return {
|
|
redirect: {
|
|
permanent: false,
|
|
destination: redirectUrl || "/",
|
|
},
|
|
} as const;
|
|
}
|
|
|
|
const props = {
|
|
redirectUrl,
|
|
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
|
isSAMLLoginEnabled,
|
|
prepopulateFormValues: undefined,
|
|
emailVerificationEnabled,
|
|
onboardingV3Enabled,
|
|
};
|
|
|
|
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) {
|
|
return {
|
|
redirect: {
|
|
permanent: false,
|
|
destination: `/auth/error?error=Signup is disabled in this instance`,
|
|
},
|
|
} as const;
|
|
}
|
|
|
|
// no token given, treat as a normal signup without verification token
|
|
if (!token) {
|
|
// username + email prepopulated from query params
|
|
const queryData = querySchema.safeParse(ctx.query);
|
|
return {
|
|
props: JSON.parse(
|
|
JSON.stringify({
|
|
...props,
|
|
prepopulateFormValues: {
|
|
username: queryData.success ? queryData.data.username : null,
|
|
email: queryData.success ? queryData.data.email : null,
|
|
},
|
|
})
|
|
),
|
|
};
|
|
}
|
|
|
|
const verificationToken = await prisma.verificationToken.findUnique({
|
|
where: {
|
|
token,
|
|
},
|
|
include: {
|
|
team: {
|
|
select: {
|
|
metadata: true,
|
|
isOrganization: true,
|
|
parentId: true,
|
|
parent: {
|
|
select: {
|
|
slug: true,
|
|
isOrganization: true,
|
|
organizationSettings: true,
|
|
},
|
|
},
|
|
slug: true,
|
|
organizationSettings: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!verificationToken || verificationToken.expires < new Date()) {
|
|
return {
|
|
redirect: {
|
|
permanent: false,
|
|
destination: `/auth/error?error=Verification Token is missing or has expired`,
|
|
},
|
|
} as const;
|
|
}
|
|
|
|
if (!verificationToken?.team) {
|
|
return {
|
|
redirect: {
|
|
permanent: false,
|
|
destination: `/auth/error?error=Verification Token is not associated with any team`,
|
|
},
|
|
} as const;
|
|
}
|
|
|
|
const isValidEmail = checkValidEmail(verificationToken.identifier);
|
|
if (isValidEmail) {
|
|
const existingUser = await prisma.user.findFirst({
|
|
where: {
|
|
AND: [
|
|
{
|
|
email: verificationToken.identifier,
|
|
},
|
|
{
|
|
emailVerified: {
|
|
not: null,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
if (existingUser) {
|
|
return {
|
|
redirect: {
|
|
permanent: false,
|
|
destination: `/auth/login?callbackUrl=${WEBAPP_URL}/${ctx.query.callbackUrl}`,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
const guessUsernameFromEmail = (email: string) => {
|
|
const [username] = email.split("@");
|
|
return username;
|
|
};
|
|
|
|
let username = isValidEmail ? guessUsernameFromEmail(verificationToken.identifier) : "";
|
|
|
|
const tokenTeam = {
|
|
...verificationToken.team,
|
|
metadata: teamMetadataSchema.parse(verificationToken.team.metadata ?? null),
|
|
};
|
|
|
|
const isATeamInOrganization = tokenTeam?.parentId !== null;
|
|
// Detect if the team is an org by either the metadata flag or if it has a parent team
|
|
const isOrganization = tokenTeam.isOrganization;
|
|
const isOrganizationOrATeamInOrganization = isOrganization || isATeamInOrganization;
|
|
// If we are dealing with an org, the slug may come from the team itself or its parent
|
|
const orgSlug = isOrganizationOrATeamInOrganization
|
|
? tokenTeam.metadata?.requestedSlug || tokenTeam.parent?.slug || tokenTeam.slug
|
|
: null;
|
|
|
|
// Org context shouldn't check if a username is premium
|
|
if (!IS_SELF_HOSTED && !isOrganizationOrATeamInOrganization && username) {
|
|
// Im not sure we actually hit this because of next redirects signup to website repo - but just in case this is pretty cool :)
|
|
const { available, suggestion } = await checkPremiumUsername(username);
|
|
|
|
username = available ? username : suggestion || username;
|
|
}
|
|
|
|
const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
|
|
const parentOrgSettings = tokenTeam?.parent?.organizationSettings ?? null;
|
|
|
|
return {
|
|
props: {
|
|
...props,
|
|
token,
|
|
prepopulateFormValues:
|
|
!isOrgInviteByLink && isValidEmail
|
|
? {
|
|
email: verificationToken.identifier,
|
|
username: isOrganizationOrATeamInOrganization
|
|
? getOrgUsernameFromEmail(
|
|
verificationToken.identifier,
|
|
(isOrganization
|
|
? tokenTeam.organizationSettings?.orgAutoAcceptEmail
|
|
: parentOrgSettings?.orgAutoAcceptEmail) || ""
|
|
)
|
|
: slugify(username),
|
|
}
|
|
: null,
|
|
orgSlug,
|
|
orgAutoAcceptEmail: isOrgInviteByLink
|
|
? (tokenTeam?.organizationSettings?.orgAutoAcceptEmail ??
|
|
parentOrgSettings?.orgAutoAcceptEmail ??
|
|
null)
|
|
: null,
|
|
},
|
|
};
|
|
};
|