diff --git a/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.test.ts b/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.test.ts index 47797ddfa4..825766b47c 100644 --- a/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.test.ts +++ b/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.test.ts @@ -1,17 +1,15 @@ -import type { Mock } from "vitest"; -import { vi } from "vitest"; - import type { MockResponse } from "@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks"; - import { prismaMock, resetPrismaMock, } from "@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"; -import { - createMockTeam, - createMockFoundToken, -} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories"; import type { SignupBody } from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories"; +import { + createMockFoundToken, + createMockTeam, +} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories"; +import type { Mock } from "vitest"; +import { vi } from "vitest"; const mockFindTokenByToken: Mock = vi.fn(); const mockValidateAndGetCorrectedUsernameForTeam: Mock = vi.fn(); @@ -77,6 +75,22 @@ vi.mock("@calcom/features/watchlist/lib/telemetry", () => ({ sentrySpan: {} })); vi.mock("@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller", () => ({ checkIfEmailIsBlockedInWatchlistController: vi.fn().mockResolvedValue(false), })); +vi.mock("@calcom/features/di/containers/FeatureRepository", () => ({ + getFeatureRepository: vi.fn().mockReturnValue({ + checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(false), + }), +})); +vi.mock("@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository", () => { + return { + GlobalWatchlistRepository: class { + findBlockedEmail = vi.fn().mockResolvedValue(null); + createEntry = vi.fn().mockResolvedValue({}); + }, + }; +}); +vi.mock("@calcom/features/watchlist/lib/utils/normalization", () => ({ + normalizeEmail: vi.fn((e: string) => e.toLowerCase()), +})); vi.mock("@calcom/web/lib/buildLegacyCtx", () => ({ buildLegacyRequest: vi.fn() })); vi.mock("@calcom/features/auth/signup/utils/organization", () => ({ joinAnyChildTeamOnOrgInvite: vi.fn() })); vi.mock("@calcom/features/auth/signup/utils/token", () => ({ diff --git a/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts b/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts index 3be4611997..2c0b63f0e1 100644 --- a/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts +++ b/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts @@ -1,14 +1,23 @@ -import { cookies, headers } from "next/headers"; -import { NextResponse } from "next/server"; - +import process from "node:process"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; import { getLocaleFromRequest } from "@calcom/features/auth/lib/getLocaleFromRequest"; import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; +import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants"; import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships"; +import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization"; import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar"; +import { + findTokenByToken, + throwIfTokenExpired, + validateAndGetCorrectedUsernameForTeam, +} from "@calcom/features/auth/signup/utils/token"; import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/features/auth/signup/utils/validateUsername"; import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing"; +import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository"; +import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; +import { GlobalWatchlistRepository } from "@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository"; import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry"; +import { normalizeEmail } from "@calcom/features/watchlist/lib/utils/normalization"; import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller"; import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -19,19 +28,17 @@ import type { CustomNextApiHandler } from "@calcom/lib/server/username"; import { usernameHandler } from "@calcom/lib/server/username"; import { getTrackingFromCookies } from "@calcom/lib/tracking"; import prisma from "@calcom/prisma"; -import { CreationSource } from "@calcom/prisma/enums"; -import { IdentityProvider } from "@calcom/prisma/enums"; +import { + CreationSource, + IdentityProvider, + WatchlistAction, + WatchlistSource, + WatchlistType, +} from "@calcom/prisma/enums"; import { signupSchema } from "@calcom/prisma/zod-utils"; import { buildLegacyRequest } from "@calcom/web/lib/buildLegacyCtx"; - -import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization"; -import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants"; - -import { - findTokenByToken, - throwIfTokenExpired, - validateAndGetCorrectedUsernameForTeam, -} from "@calcom/features/auth/signup/utils/token"; +import { cookies, headers } from "next/headers"; +import { NextResponse } from "next/server"; const log = logger.getSubLogger({ prefix: ["signupCalcomHandler"] }); @@ -291,6 +298,34 @@ const handler: CustomNextApiHandler = async (body, usernameStatus, query) => { } } + const featureRepository = getFeatureRepository(); + const signupWatchlistReviewEnabled = + await featureRepository.checkIfFeatureIsEnabledGlobally("signup-watchlist-review"); + + if (signupWatchlistReviewEnabled && !token) { + const globalWatchlistRepo = new GlobalWatchlistRepository(prisma); + const normalizedEmail = normalizeEmail(email); + const existing = await globalWatchlistRepo.findBlockedEmail(normalizedEmail); + + if (!existing) { + await globalWatchlistRepo.createEntry({ + type: WatchlistType.EMAIL, + value: normalizedEmail, + action: WatchlistAction.BLOCK, + source: WatchlistSource.SIGNUP, + description: "Auto-added during signup review", + }); + } + + const userRepository = new UserRepository(prisma); + await userRepository.lockByEmail({ email }); + + return NextResponse.json( + { message: "Created user", stripeCustomerId: customer.stripeCustomerId, accountUnderReview: true }, + { status: 201 } + ); + } + if (checkoutSessionId) { console.log("Created user but missing payment", checkoutSessionId); return NextResponse.json( diff --git a/apps/web/modules/feature-flags/hooks/useFlags.ts b/apps/web/modules/feature-flags/hooks/useFlags.ts index 59e6438255..9038e72b23 100644 --- a/apps/web/modules/feature-flags/hooks/useFlags.ts +++ b/apps/web/modules/feature-flags/hooks/useFlags.ts @@ -37,6 +37,7 @@ const initialData: AppFlags = { "hwm-seating": false, "active-user-billing": false, "sidebar-tips": false, + "signup-watchlist-review": false, }; if (process.env.NEXT_PUBLIC_IS_E2E) { diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index 26f6313011..e72fbde1b3 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -1,35 +1,22 @@ "use client"; -import { Analytics as DubAnalytics } from "@dub/analytics/react"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { signIn } from "next-auth/react"; -import dynamic from "next/dynamic"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import Script from "next/script"; -import posthog from "posthog-js"; -import { useState, useEffect } from "react"; -import type { SubmitHandler } from "react-hook-form"; -import { useForm, useFormContext } from "react-hook-form"; -import { Toaster } from "sonner"; -import { z } from "zod"; - import getStripe from "@calcom/app-store/stripepayment/lib/client"; import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils"; import { fetchSignup, - isUserAlreadyExistsError, hasCheckoutSession, + isAccountUnderReview, + isUserAlreadyExistsError, } from "@calcom/features/auth/signup/lib/fetchSignup"; import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail"; import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import ServerTrans from "@calcom/lib/components/ServerTrans"; import { APP_NAME, - URL_PROTOCOL_REGEX, - IS_CALCOM, - WEBAPP_URL, CLOUDFLARE_SITE_ID, + IS_CALCOM, + URL_PROTOCOL_REGEX, + WEBAPP_URL, WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL, WEBSITE_URL, @@ -47,12 +34,24 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import classNames from "@calcom/ui/classNames"; import { Alert } from "@calcom/ui/components/alert"; import { Button } from "@calcom/ui/components/button"; -import { PasswordField, CheckboxField, TextField, Form, SelectField } from "@calcom/ui/components/form"; +import { CheckboxField, Form, PasswordField, SelectField, TextField } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; -import { InfoIcon, ShieldCheckIcon, StarIcon } from "@coss/ui/icons"; import { showToast } from "@calcom/ui/components/toast"; - +import { InfoIcon, ShieldCheckIcon, StarIcon } from "@coss/ui/icons"; +import { Analytics as DubAnalytics } from "@dub/analytics/react"; +import { zodResolver } from "@hookform/resolvers/zod"; import type { getServerSideProps } from "@lib/signup/getServerSideProps"; +import dynamic from "next/dynamic"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import Script from "next/script"; +import { signIn } from "next-auth/react"; +import posthog from "posthog-js"; +import { useEffect, useState } from "react"; +import type { SubmitHandler } from "react-hook-form"; +import { useForm, useFormContext } from "react-hook-form"; +import { Toaster } from "sonner"; +import { z } from "zod"; const signupSchema = apiSignupSchema.extend({ apiError: z.string().optional(), // Needed to display API errors doesn't get passed to the API @@ -199,6 +198,7 @@ export default function Signup({ const [premiumUsername, setPremiumUsername] = useState(false); const [usernameTaken, setUsernameTaken] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false); + const [accountUnderReview, setAccountUnderReview] = useState(false); const [displayEmailForm, setDisplayEmailForm] = useState(token); const [turnstileKey, setTurnstileKey] = useState(0); const searchParams = useCompatSearchParams(); @@ -277,6 +277,11 @@ export default function Signup({ throw new Error(result.error.message); } + if (isAccountUnderReview(result)) { + setAccountUnderReview(true); + return; + } + if (process.env.NEXT_PUBLIC_GTM_ID) { pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language }); } @@ -378,388 +383,409 @@ export default function Signup({
{/* Left side */}
- {displayBackButton && ( -
-
- )} -
-

- {IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")} -

- {IS_CALCOM ? ( -

{t("cal_signup_description")}

- ) : ( -

- {t("calcom_explained", { - appName: APP_NAME, - })} -

- )} - {IS_CALCOM && ( -
- { - if (option && "value" in option) { - const currentUrl = new URL(window.location.href); - - // Handle localhost - add region as URL parameter - if (currentUrl.hostname === "localhost") { - currentUrl.searchParams.set("region", option.value); - window.location.href = currentUrl.toString(); - return; - } - - // Handle production domains - modify hostname only to preserve query params - if (option.value === "eu") { - currentUrl.hostname = currentUrl.hostname.replace("cal.com", "cal.eu"); - } else { - currentUrl.hostname = currentUrl.hostname.replace("cal.eu", "cal.com"); - } - window.location.href = currentUrl.toString(); - } - }} - /> -
- )} -
- - {/* Form Container */} - {displayEmailForm && ( -
-
{ - let updatedValues = values; - if (!formMethods.getValues().username && isOrgInviteByLink) { - updatedValues = { - ...values, - username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail), - }; - } - await signUp(updatedValues); - }}> - {/* Username */} - {!isOrgInviteByLink ? ( - setUsernameTaken(value)} - data-testid="signup-usernamefield" - setPremium={(value) => setPremiumUsername(value)} - addOnLeading={ - orgSlug - ? truncateDomain( - `${getOrgFullOrigin(orgSlug, { protocol: true }).replace( - URL_PROTOCOL_REGEX, - "" - )}/` - ) - : truncateDomain( - `${process.env.NEXT_PUBLIC_WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/` - ) - } - /> - ) : null} - {/* Email */} - - - {/* Password */} - {!isSamlSignup && ( - - )} - {/* Cloudflare Turnstile Captcha */} - {CLOUDFLARE_SITE_ID ? ( - { - formMethods.setValue("cfToken", token); - }} - onExpire={() => { - formMethods.setValue("cfToken", undefined); - }} - onError={() => { - formMethods.setValue("cfToken", undefined); - }} - /> - ) : null} - - handleConsentChange(userConsentToCookie)} - description={t("cookie_consent_checkbox")} - /> - {errors.apiError && ( - - )} - {isSamlSignup ? ( + ) : ( + <> + {displayBackButton && ( +
- ) : ( - - )} - -
- )} - {!displayEmailForm && ( -
- {/* Upper Row */} - {isGoogleLoginEnabled && ( -
-
)} +
+

+ {IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")} +

+ {IS_CALCOM ? ( +

+ {t("cal_signup_description")} +

+ ) : ( +

+ {t("calcom_explained", { + appName: APP_NAME, + })} +

+ )} + {IS_CALCOM && ( +
+ { + if (option && "value" in option) { + const currentUrl = new URL(window.location.href); - {isGoogleLoginEnabled && ( -
-
-
- - {t("or").toLocaleLowerCase()} - -
+ // Handle localhost - add region as URL parameter + if (currentUrl.hostname === "localhost") { + currentUrl.searchParams.set("region", option.value); + window.location.href = currentUrl.toString(); + return; + } + + // Handle production domains - modify hostname only to preserve query params + if (option.value === "eu") { + currentUrl.hostname = currentUrl.hostname.replace("cal.com", "cal.eu"); + } else { + currentUrl.hostname = currentUrl.hostname.replace("cal.eu", "cal.com"); + } + window.location.href = currentUrl.toString(); + } + }} + /> +
+ )} +
+ + {/* Form Container */} + {displayEmailForm && ( +
+
{ + let updatedValues = values; + if (!formMethods.getValues().username && isOrgInviteByLink) { + updatedValues = { + ...values, + username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail), + }; + } + await signUp(updatedValues); + }}> + {/* Username */} + {!isOrgInviteByLink ? ( + setUsernameTaken(value)} + data-testid="signup-usernamefield" + setPremium={(value) => setPremiumUsername(value)} + addOnLeading={ + orgSlug + ? truncateDomain( + `${getOrgFullOrigin(orgSlug, { protocol: true }).replace( + URL_PROTOCOL_REGEX, + "" + )}/` + ) + : truncateDomain( + `${process.env.NEXT_PUBLIC_WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/` + ) + } + /> + ) : null} + {/* Email */} + + + {/* Password */} + {!isSamlSignup && ( + + )} + {/* Cloudflare Turnstile Captcha */} + {CLOUDFLARE_SITE_ID ? ( + { + formMethods.setValue("cfToken", token); + }} + onExpire={() => { + formMethods.setValue("cfToken", undefined); + }} + onError={() => { + formMethods.setValue("cfToken", undefined); + }} + /> + ) : null} + + handleConsentChange(userConsentToCookie)} + description={t("cookie_consent_checkbox")} + /> + {errors.apiError && ( + + )} + {isSamlSignup ? ( + + ) : ( + + )} + +
+ )} + {!displayEmailForm && ( +
+ {/* Upper Row */} + {isGoogleLoginEnabled && ( +
+ +
+ )} + + {isGoogleLoginEnabled && ( +
+
+
+ + {t("or").toLocaleLowerCase()} + +
+
+
+ )} + + {/* Lower Row */} +
+ + {isSAMLLoginEnabled && ( + + )}
)} - {/* Lower Row */} -
- - {isSAMLLoginEnabled && ( - - )} + {/* Already have an account & T&C */} +
+
+
+

{t("already_have_account")}

+ + {t("sign_in")} + +
+
+ + Terms + , + + Privacy Policy. + , + ]} + /> +
+
-
+ )} - - {/* Already have an account & T&C */} -
-
-
-

{t("already_have_account")}

- - {t("sign_in")} - -
-
- - Terms - , - - Privacy Policy. - , - ]} - /> -
-
-
{IS_CALCOM && ( diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 3bdf264f1c..783a940ded 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2310,6 +2310,8 @@ "enable_apps": "Enable apps", "enable_apps_description": "Enable apps that users can integrate with {{appName}}", "purchase_license": "Purchase a license", + "account_under_review_title": "Your account is under review", + "account_under_review_description": "Your account has been created and is currently under review. You will be able to sign in once your account has been approved.", "already_have_account": "Already have an account?", "already_have_key": "I already have a key:", "already_have_key_suggestion": "Please copy your existing CALCOM_LICENSE_KEY environment variable here.", diff --git a/packages/features/auth/signup/lib/fetchSignup.test.ts b/packages/features/auth/signup/lib/fetchSignup.test.ts index d1d394cee3..66099977ed 100644 --- a/packages/features/auth/signup/lib/fetchSignup.test.ts +++ b/packages/features/auth/signup/lib/fetchSignup.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { SIGNUP_ERROR_CODES } from "../constants"; -import { fetchSignup, isUserAlreadyExistsError, hasCheckoutSession } from "./fetchSignup"; +import { fetchSignup, isUserAlreadyExistsError, hasCheckoutSession, isAccountUnderReview } from "./fetchSignup"; function createJsonResponse(json: unknown, status = 200) { return new Response(JSON.stringify(json), { @@ -101,3 +101,42 @@ describe("hasCheckoutSession", () => { expect(hasCheckoutSession(result)).toBe(false); }); }); + +describe("isAccountUnderReview", () => { + it("returns true when accountUnderReview is true in success response", () => { + const result = { + ok: true as const, + data: { message: "Created user", accountUnderReview: true }, + }; + + expect(isAccountUnderReview(result)).toBe(true); + }); + + it("returns false when accountUnderReview is not set", () => { + const result = { + ok: true as const, + data: { message: "Created user" }, + }; + + expect(isAccountUnderReview(result)).toBe(false); + }); + + it("returns false when accountUnderReview is false", () => { + const result = { + ok: true as const, + data: { message: "Created user", accountUnderReview: false }, + }; + + expect(isAccountUnderReview(result)).toBe(false); + }); + + it("returns false for error responses", () => { + const result = { + ok: false as const, + status: 500, + error: { message: "Internal error" }, + }; + + expect(isAccountUnderReview(result)).toBe(false); + }); +}); diff --git a/packages/features/auth/signup/lib/fetchSignup.ts b/packages/features/auth/signup/lib/fetchSignup.ts index 8333c0c5c8..3e2b53afa0 100644 --- a/packages/features/auth/signup/lib/fetchSignup.ts +++ b/packages/features/auth/signup/lib/fetchSignup.ts @@ -11,6 +11,7 @@ type SignupData = { type SignupSuccessResponse = { message: string; stripeCustomerId?: string; + accountUnderReview?: boolean; }; type SignupErrorResponse = { @@ -20,9 +21,9 @@ type SignupErrorResponse = { type SignupResponse = SignupSuccessResponse | SignupErrorResponse; -export type SignupResult = - | { ok: true; data: SignupSuccessResponse } - | { ok: false; status: number; error: SignupErrorResponse }; +export type SignupSuccessResult = { ok: true; data: SignupSuccessResponse }; + +export type SignupResult = SignupSuccessResult | { ok: false; status: number; error: SignupErrorResponse }; export async function fetchSignup(data: SignupData, cfToken?: string): Promise { const allParams = new URLSearchParams(window.location.search); @@ -91,3 +92,7 @@ export function hasCheckoutSession( ): result is { ok: false; status: number; error: SignupErrorResponse & { checkoutSessionId: string } } { return !result.ok && !!result.error.checkoutSessionId; } + +export function isAccountUnderReview(result: SignupResult): boolean { + return result.ok && result.data.accountUnderReview === true; +} diff --git a/packages/features/flags/config.ts b/packages/features/flags/config.ts index 904698e6c6..2430d39ba0 100644 --- a/packages/features/flags/config.ts +++ b/packages/features/flags/config.ts @@ -38,6 +38,7 @@ export type AppFlags = { "hwm-seating": boolean; "active-user-billing": boolean; "sidebar-tips": boolean; + "signup-watchlist-review": boolean; }; export type TeamFeatures = Record; diff --git a/packages/features/users/repositories/UserRepository.ts b/packages/features/users/repositories/UserRepository.ts index c526d8b6be..8458b3876a 100644 --- a/packages/features/users/repositories/UserRepository.ts +++ b/packages/features/users/repositories/UserRepository.ts @@ -1429,4 +1429,11 @@ export class UserRepository { }, }); } + + async lockByEmail({ email }: { email: string }) { + await this.prismaClient.user.updateMany({ + where: { email }, + data: { locked: true }, + }); + } } diff --git a/packages/prisma/migrations/20260212202500_add_signup_watchlist_review_feature/migration.sql b/packages/prisma/migrations/20260212202500_add_signup_watchlist_review_feature/migration.sql new file mode 100644 index 0000000000..3f36a4ba1e --- /dev/null +++ b/packages/prisma/migrations/20260212202500_add_signup_watchlist_review_feature/migration.sql @@ -0,0 +1,8 @@ +-- Add SIGNUP to WatchlistSource enum +ALTER TYPE "WatchlistSource" ADD VALUE IF NOT EXISTS 'SIGNUP'; + +-- Seed the signup-watchlist-review feature flag +INSERT INTO "Feature" ("slug", "enabled", "type", "description", "createdAt", "updatedAt") +VALUES + ('signup-watchlist-review', false, 'OPERATIONAL', 'When enabled, new signups are added to the watchlist for review', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +ON CONFLICT ("slug") DO NOTHING; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 3f233473fa..38ac05a73c 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -2464,6 +2464,7 @@ enum WatchlistAction { enum WatchlistSource { MANUAL FREE_DOMAIN_POLICY + SIGNUP } model Watchlist { @@ -3044,7 +3045,7 @@ model TeamBilling { subscriptionEnd DateTime? billingPeriod BillingPeriod? - billingMode BillingMode @default(SEATS) + billingMode BillingMode @default(SEATS) pricePerSeat Int? paidSeats Int? monthlyProrations MonthlyProration[] @@ -3073,7 +3074,7 @@ model OrganizationBilling { subscriptionEnd DateTime? billingPeriod BillingPeriod? - billingMode BillingMode @default(SEATS) + billingMode BillingMode @default(SEATS) pricePerSeat Int? paidSeats Int? monthlyProrations MonthlyProration[] diff --git a/packages/trpc/server/routers/viewer/admin/lockUserAccount.handler.ts b/packages/trpc/server/routers/viewer/admin/lockUserAccount.handler.ts index 1496142253..e2ba7b7063 100644 --- a/packages/trpc/server/routers/viewer/admin/lockUserAccount.handler.ts +++ b/packages/trpc/server/routers/viewer/admin/lockUserAccount.handler.ts @@ -1,3 +1,5 @@ +import { GlobalWatchlistRepository } from "@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository"; +import { normalizeEmail } from "@calcom/features/watchlist/lib/utils/normalization"; import { prisma } from "@calcom/prisma"; import type { TrpcSessionUser } from "../../../types"; @@ -20,12 +22,25 @@ const lockUserAccountHandler = async ({ input }: GetOptions) => { data: { locked, }, + select: { + id: true, + email: true, + }, }); if (!user) { throw new Error("User not found"); } + if (!locked) { + const globalWatchlistRepo = new GlobalWatchlistRepository(prisma); + const normalizedEmail = normalizeEmail(user.email); + const watchlistEntry = await globalWatchlistRepo.findBlockedEmail(normalizedEmail); + if (watchlistEntry) { + await globalWatchlistRepo.deleteEntry(watchlistEntry.id); + } + } + return { success: true, userId,