* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main: - Rebrand Cal.com to Cal.diy across the entire codebase - Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions - Switch license from AGPL-3.0 to MIT - Remove docs/ directory (migrated to Nextra at cal.diy) - Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc. - Clean up .env.example for self-hosted Cal.diy - Update Docker image references to calcom/cal.diy - Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork - Add PR welcome bot for Cal.diy contributors - Fix API v2 breaking changes oasdiff ignore entries - Replace Blacksmith CI runners with default GitHub Actions 3893 files changed, 20789 insertions(+), 411020 deletions(-) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rip out org related comments in api v2 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
223 lines
6.8 KiB
TypeScript
223 lines
6.8 KiB
TypeScript
import process from "node:process";
|
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
|
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";
|
|
import type { GetServerSidePropsContext } from "next";
|
|
import { z } from "zod";
|
|
|
|
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,
|
|
|
|
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 = true;
|
|
const suggestion: string | undefined = undefined;
|
|
|
|
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,
|
|
},
|
|
};
|
|
};
|