Files
calendar/packages/features/auth/lib/verifyEmail.ts
T
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>morgan@cal.com <morgan@cal.com>Morgan
1d1a242a72 refactor: convert getShouldServeCache to CacheService with dependency injection (#22814)
* 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>
2025-07-31 20:39:27 +03:00

172 lines
4.8 KiB
TypeScript

import { randomBytes, createHash } from "crypto";
import { totp } from "otplib";
import {
sendEmailVerificationCode,
sendEmailVerificationLink,
sendChangeOfEmailVerificationLink,
} from "@calcom/emails/email-manager";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
const log = logger.getSubLogger({ prefix: [`[[Auth] `] });
interface VerifyEmailType {
username?: string;
email: string;
language?: string;
secondaryEmailId?: number;
isVerifyingEmail?: boolean;
isPlatform?: boolean;
}
export const sendEmailVerification = async ({
email,
language,
username,
secondaryEmailId,
isPlatform = false,
}: VerifyEmailType) => {
const token = randomBytes(32).toString("hex");
const translation = await getTranslation(language ?? "en", "common");
const featuresRepository = new FeaturesRepository(prisma);
const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
if (!emailVerification) {
log.warn("Email verification is disabled - Skipping");
return { ok: true, skipped: true };
}
if (await checkIfEmailIsBlockedInWatchlistController(email)) {
log.warn("Email is blocked - not sending verification email", email);
return { ok: false, skipped: false };
}
if (isPlatform) {
log.warn("Skipping Email verification");
return { ok: true, skipped: true };
}
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: email,
});
await prisma.verificationToken.create({
data: {
identifier: email,
token,
expires: new Date(Date.now() + 24 * 3600 * 1000), // +1 day
secondaryEmailId: secondaryEmailId || null,
},
});
const params = new URLSearchParams({
token,
});
await sendEmailVerificationLink({
language: translation,
verificationEmailLink: `${WEBAPP_URL}/api/auth/verify-email?${params.toString()}`,
user: {
email,
name: username,
},
isSecondaryEmailVerification: !!secondaryEmailId,
});
return { ok: true, skipped: false };
};
export const sendEmailVerificationByCode = async ({
email,
language,
username,
isVerifyingEmail,
}: VerifyEmailType) => {
if (await checkIfEmailIsBlockedInWatchlistController(email)) {
log.warn("Email is blocked - not sending verification email", email);
return { ok: false, skipped: false };
}
const translation = await getTranslation(language ?? "en", "common");
const secret = createHash("md5")
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
.digest("hex");
totp.options = { step: 900 };
const code = totp.generate(secret);
await sendEmailVerificationCode({
language: translation,
verificationEmailCode: code,
user: {
email,
name: username,
},
isVerifyingEmail,
});
return { ok: true, skipped: false };
};
interface ChangeOfEmail {
user: {
username: string;
emailFrom: string;
emailTo: string;
};
language?: string;
}
export const sendChangeOfEmailVerification = async ({ user, language }: ChangeOfEmail) => {
const token = randomBytes(32).toString("hex");
const translation = await getTranslation(language ?? "en", "common");
const featuresRepository = new FeaturesRepository(prisma);
const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
if (!emailVerification) {
log.warn("Email verification is disabled - Skipping");
return { ok: true, skipped: true };
}
if (await checkIfEmailIsBlockedInWatchlistController(user.emailFrom)) {
log.warn("Email is blocked - not sending verification email", user.emailFrom);
return { ok: false, skipped: false };
}
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: user.emailFrom,
});
await prisma.verificationToken.create({
data: {
identifier: user.emailFrom, // We use from as this is the email use to get the metadata from
token,
expires: new Date(Date.now() + 24 * 3600 * 1000), // +1 day
},
});
const params = new URLSearchParams({
token,
});
await sendChangeOfEmailVerificationLink({
language: translation,
verificationEmailLink: `${WEBAPP_URL}/auth/verify-email-change?${params.toString()}`,
user: {
emailFrom: user.emailFrom,
emailTo: user.emailTo,
name: user.username,
},
});
return { ok: true, skipped: false };
};