* refactor: convert getShouldServeCache to CacheService with dependency injection - Create CacheService class following AvailableSlotsService DI pattern - Add FeaturesRepository and CacheService to DI tokens and modules - Create cache container with proper dependency injection setup - Update handleNewBooking.ts and slots/util.ts to use new service - Maintain backward compatibility with error-throwing wrapper function - Follow established service patterns for clean architecture Co-Authored-By: morgan@cal.com <morgan@cal.com> * feat: inject CacheService into AvailableSlotsService via dependency injection - Add cacheService to IAvailableSlotsService interface - Update available-slots container to load cache modules - Update available-slots module to inject CacheService dependency - Replace direct getShouldServeCache call with injected service method - Add CacheService import to util.ts for proper typing Co-Authored-By: morgan@cal.com <morgan@cal.com> * chore: DI api v2 cache service * refactor: convert FeaturesRepository to use factory pattern in DI - Change from constructor injection to factory pattern to avoid PRISMA_CLIENT binding issues in tests - FeaturesRepository now uses default prisma instance instead of DI injection - Resolves test failures while maintaining DI container compatibility - Tests reduced from 123+ failures to only 5 unrelated failures Co-Authored-By: morgan@cal.com <morgan@cal.com> * revert: use direct FeaturesRepository instantiation in most usage points - Revert getFeaturesRepository() calls back to new FeaturesRepository() - Tests require direct instantiation for mocking compatibility - Keep DI container for specific use cases that need dependency injection - Resolves test failures while maintaining both DI and direct usage patterns Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: resolve FeaturesRepository DI container issues - Update cache module to use factory pattern with proper ICacheService interface - Remove featuresModule loading from cache and available-slots containers - Use direct FeaturesRepository instantiation via getFeaturesRepository() - Resolves 'No binding found for key: Symbol(FeaturesRepository)' errors - Reduces test failures from 125 to 7 (remaining failures appear unrelated) Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: update all FeaturesRepository instantiations to include prisma parameter - Add prisma parameter to all new FeaturesRepository() calls across the codebase - Update API v2 services to match main repo interfaces - Fix PrismaFeaturesRepository to implement IFeaturesRepository directly - Update CacheService in API v2 to expose required dependencies and getShouldServeCache - Implement CheckBookingLimitsService in API v2 with proper interface - Resolves type assignment errors between API v2 and main repo implementations Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: add prisma parameter to remaining FeaturesRepository instantiations in apps/web/lib - Update getServerSideProps files to pass prisma parameter to FeaturesRepository - Ensures all FeaturesRepository instantiations follow the new constructor pattern - Completes the refactoring to use direct instantiation with prisma parameter Co-Authored-By: morgan@cal.com <morgan@cal.com> * refactor clean and fix devin issues * chore: bump platform libs * chore: bump platform libs * chore: bump platform libs * chore: bump platform libs * fix: missing di * fix workflow test * fix workflow test * fix integration test --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: morgan@cal.com <morgan@cal.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
193 lines
6.1 KiB
TypeScript
193 lines
6.1 KiB
TypeScript
import type { GetServerSidePropsContext } from "next";
|
|
import { z } from "zod";
|
|
|
|
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 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 props = {
|
|
redirectUrl,
|
|
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
|
isSAMLLoginEnabled,
|
|
prepopulateFormValues: undefined,
|
|
emailVerificationEnabled,
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
const existingUser = await prisma.user.findFirst({
|
|
where: {
|
|
AND: [
|
|
{
|
|
email: verificationToken?.identifier,
|
|
},
|
|
{
|
|
emailVerified: {
|
|
not: null,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
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 = guessUsernameFromEmail(verificationToken.identifier);
|
|
|
|
const tokenTeam = {
|
|
...verificationToken?.team,
|
|
metadata: teamMetadataSchema.parse(verificationToken?.team?.metadata),
|
|
};
|
|
|
|
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) {
|
|
// 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 isValidEmail = checkValidEmail(verificationToken.identifier);
|
|
const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
|
|
const parentOrgSettings = tokenTeam?.parent?.organizationSettings ?? null;
|
|
|
|
return {
|
|
props: {
|
|
...props,
|
|
token,
|
|
prepopulateFormValues: !isOrgInviteByLink
|
|
? {
|
|
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,
|
|
},
|
|
};
|
|
};
|