Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -4340,5 +4340,15 @@
"pbac_resource_feature_opt_in": "Feature Opt-In",
"pbac_desc_view_feature_opt_in": "View feature opt-in settings",
"pbac_desc_update_feature_opt_in": "Manage feature opt-in settings",
"user_locked_email_subject": "Your {{appName}} account has been locked",
"user_locked_email_body": "Your account has been temporarily locked due to suspicious activity detected on your account.",
"user_locked_email_reason": "Reason",
"user_locked_email_false_alarm": "If you believe this is a mistake or a false alarm, please contact our support team immediately to restore access to your account.",
"user_locked_email_contact_support": "Contact support",
"user_locked_email_contact_support_button": "Contact Support",
"user_locked_email_regards": "Best regards",
"user_locked_reason_rate_limit": "Your account exceeded the rate limit threshold multiple times, which may indicate automated or abusive behavior.",
"user_locked_reason_spam_workflow": "Spam content was detected in your workflow configuration.",
"user_locked_reason_unknown": "Suspicious activity was detected on your account.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS":"↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
1 change: 1 addition & 0 deletions apps/web/test/lib/getSchedule/futureLimit.timezone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ vi.mock("@calcom/lib/constants", () => ({
RESERVED_SUBDOMAINS: ["auth", "docs"],
ROLLING_WINDOW_PERIOD_MAX_DAYS_TO_CHECK: 61,
SINGLE_ORG_SLUG: "",
ENABLE_ASYNC_TASKER: false,
}));

describe("getSchedule", () => {
Expand Down
62 changes: 62 additions & 0 deletions packages/emails/src/templates/UserLockedEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { LockReason } from "@calcom/features/ee/api-keys/lib/autoLock";
import { APP_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import type { TFunction } from "i18next";

import { BaseEmailHtml, CallToAction } from "../components";

export type UserLockedEmailProps = {
language: TFunction;
user: {
name?: string | null;
email: string;
};
lockReason: LockReason;
};

const getLockReasonText = (lockReason: LockReason, language: TFunction): string => {
switch (lockReason) {
case LockReason.RATE_LIMIT:
return language("user_locked_reason_rate_limit");
case LockReason.SPAM_WORKFLOW_BODY:
return language("user_locked_reason_spam_workflow");
default:
return language("user_locked_reason_unknown");
}
};

export const UserLockedEmail = (
props: UserLockedEmailProps & Partial<React.ComponentProps<typeof BaseEmailHtml>>
) => {
const { language, user, lockReason } = props;
const lockReasonText = getLockReasonText(lockReason, language);

return (
<BaseEmailHtml subject={language("user_locked_email_subject", { appName: APP_NAME })}>
<p>
<>{language("hi_user_name", { name: user.name || language("user") })}!</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<>{language("user_locked_email_body")}</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<strong>{language("user_locked_email_reason")}:</strong> {lockReasonText}
</p>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<>{language("user_locked_email_false_alarm")}</>
</p>
<CallToAction
label={language("user_locked_email_contact_support_button")}
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
/>
<div style={{ lineHeight: "6px", marginTop: "24px" }}>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<>
{language("user_locked_email_regards")},
<br />
{APP_NAME} {language("team")}
</>
</p>
</div>
</BaseEmailHtml>
);
};
45 changes: 23 additions & 22 deletions packages/emails/src/templates/index.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,46 @@
export * from "@calcom/app-store/routing-forms/emails/components";
export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificationEmail";
export { AttendeeAddGuestsEmail } from "./AttendeeAddGuestsEmail";
export { AttendeeAwaitingPaymentEmail } from "./AttendeeAwaitingPaymentEmail";
export { AttendeeCancelledEmail } from "./AttendeeCancelledEmail";
export { AttendeeCancelledSeatEmail } from "./AttendeeCancelledSeatEmail";
export { AttendeeDeclinedEmail } from "./AttendeeDeclinedEmail";
export { AttendeeLocationChangeEmail } from "./AttendeeLocationChangeEmail";
export { AttendeeRequestEmail } from "./AttendeeRequestEmail";
export { AttendeeWasRequestedToRescheduleEmail } from "./AttendeeWasRequestedToRescheduleEmail";
export { AttendeeRescheduledEmail } from "./AttendeeRescheduledEmail";
export { AttendeeUpdatedEmail } from "./AttendeeUpdatedEmail";
export { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
export { AttendeeUpdatedEmail } from "./AttendeeUpdatedEmail";
export { AttendeeWasRequestedToRescheduleEmail } from "./AttendeeWasRequestedToRescheduleEmail";
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
export { BrokenIntegrationEmail } from "./BrokenIntegrationEmail";
export { CreditBalanceLimitReachedEmail } from "./CreditBalanceLimitReachedEmail";
export { CreditBalanceLowWarningEmail } from "./CreditBalanceLowWarningEmail";
export { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail";
export { DailyVideoDownloadTranscriptEmail } from "./DailyVideoDownloadTranscriptEmail";
export { DisabledAppEmail } from "./DisabledAppEmail";
export { SlugReplacementEmail } from "./SlugReplacementEmail";
export { FeedbackEmail } from "./FeedbackEmail";
export { ForgotPasswordEmail } from "./ForgotPasswordEmail";
export { MonthlyDigestEmail } from "./MonthlyDigestEmail";
export { NoShowFeeChargedEmail } from "./NoShowFeeChargedEmail";
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
export { OrganizationAdminNoSlotsEmail } from "./OrganizationAdminNoSlots";
export { OrganizationCreationEmail } from "./OrganizationCreationEmail";
export { OrganizerAddGuestsEmail } from "./OrganizerAddGuestsEmail";
export { OrganizerAttendeeCancelledSeatEmail } from "./OrganizerAttendeeCancelledSeatEmail";
export { OrganizerCancelledEmail } from "./OrganizerCancelledEmail";
export { OrganizerReassignedEmail } from "./OrganizerReassignedEmail";
export { OrganizerLocationChangeEmail } from "./OrganizerLocationChangeEmail";
export { OrganizerPaymentRefundFailedEmail } from "./OrganizerPaymentRefundFailedEmail";
export { OrganizerReassignedEmail } from "./OrganizerReassignedEmail";
export { OrganizerRequestEmail } from "./OrganizerRequestEmail";
export { OrganizerRequestEmailV2 } from "./OrganizerRequestEmailV2";
export { OrganizerRequestReminderEmail } from "./OrganizerRequestReminderEmail";
export { OrganizerRequestedToRescheduleEmail } from "./OrganizerRequestedToRescheduleEmail";
export { OrganizerRequestReminderEmail } from "./OrganizerRequestReminderEmail";
export { OrganizerRescheduledEmail } from "./OrganizerRescheduledEmail";
export { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
export { SlugReplacementEmail } from "./SlugReplacementEmail";
export { TeamInviteEmail } from "./TeamInviteEmail";
export { BrokenIntegrationEmail } from "./BrokenIntegrationEmail";
export { CreditBalanceLowWarningEmail } from "./CreditBalanceLowWarningEmail";
export { CreditBalanceLimitReachedEmail } from "./CreditBalanceLimitReachedEmail";
export { OrganizerAttendeeCancelledSeatEmail } from "./OrganizerAttendeeCancelledSeatEmail";
export { NoShowFeeChargedEmail } from "./NoShowFeeChargedEmail";
export { UserLockedEmail } from "./UserLockedEmail";
export { VerifyAccountEmail } from "./VerifyAccountEmail";
export { VerifyEmailByCode } from "./VerifyEmailByCode";
export * from "@calcom/app-store/routing-forms/emails/components";
export { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail";
export { DailyVideoDownloadTranscriptEmail } from "./DailyVideoDownloadTranscriptEmail";
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
export { MonthlyDigestEmail } from "./MonthlyDigestEmail";
export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificationEmail";
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
export { VerifyEmailChangeEmail } from "./VerifyEmailChangeEmail";
export { OrganizationCreationEmail } from "./OrganizationCreationEmail";
export { OrganizerAddGuestsEmail } from "./OrganizerAddGuestsEmail";
export { AttendeeAddGuestsEmail } from "./AttendeeAddGuestsEmail";
export { OrganizationAdminNoSlotsEmail } from "./OrganizationAdminNoSlots";
70 changes: 70 additions & 0 deletions packages/emails/templates/user-locked-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { LockReason } from "@calcom/features/ee/api-keys/lib/autoLock";
import { APP_NAME, EMAIL_FROM_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import type { TFunction } from "i18next";

import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";

export type UserLockedEmailData = {
language: TFunction;
user: {
name?: string | null;
email: string;
};
lockReason: LockReason;
};

export default class UserLockedEmail extends BaseEmail {
userLockedEvent: UserLockedEmailData;

constructor(userLockedEvent: UserLockedEmailData) {
super();
this.name = "SEND_USER_LOCKED_EMAIL";
this.userLockedEvent = userLockedEvent;
}

protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
to: `${this.userLockedEvent.user.name} <${this.userLockedEvent.user.email}>`,
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
subject: this.userLockedEvent.language("user_locked_email_subject", {
appName: APP_NAME,
}),
html: await renderEmail("UserLockedEmail", this.userLockedEvent),
text: this.getTextBody(),
};
}

protected getTextBody(): string {
const { language, user, lockReason } = this.userLockedEvent;
const lockReasonText = this.getLockReasonText(lockReason, language);

return `
${language("user_locked_email_subject", { appName: APP_NAME })}

${language("hi_user_name", { name: user.name || language("user") })},

${language("user_locked_email_body")}

${language("user_locked_email_reason")}: ${lockReasonText}

${language("user_locked_email_false_alarm")}

${language("user_locked_email_contact_support")}: ${SUPPORT_MAIL_ADDRESS}

${language("user_locked_email_regards")},
${APP_NAME} ${language("team")}
`.replace(/(<([^>]+)>)/gi, "");
}

private getLockReasonText(lockReason: LockReason, language: TFunction): string {
switch (lockReason) {
case LockReason.RATE_LIMIT:
return language("user_locked_reason_rate_limit");
case LockReason.SPAM_WORKFLOW_BODY:
return language("user_locked_reason_spam_workflow");
default:
return language("user_locked_reason_unknown");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { UserLockedEmailSyncTasker } from "@calcom/features/ee/api-keys/service/userLockedEmail/tasker/UserLockedEmailSyncTasker";

import { USER_LOCKED_EMAIL_TASKER_DI_TOKENS } from "./tokens";

const thisModule = createModule();
const token = USER_LOCKED_EMAIL_TASKER_DI_TOKENS.USER_LOCKED_EMAIL_SYNC_TASKER;
const moduleToken = USER_LOCKED_EMAIL_TASKER_DI_TOKENS.USER_LOCKED_EMAIL_SYNC_TASKER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: UserLockedEmailSyncTasker,
dep: loggerServiceModule,
});

export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createContainer } from "@calcom/features/di/di";

import type { UserLockedEmailTasker } from "@calcom/features/ee/api-keys/service/userLockedEmail/tasker/UserLockedEmailTasker";
import { USER_LOCKED_EMAIL_TASKER_DI_TOKENS } from "./tokens";
import { moduleLoader as userLockedEmailTaskerModule } from "./UserLockedEmailTasker.module";

const container = createContainer();

export function getUserLockedEmailTasker(): UserLockedEmailTasker {
userLockedEmailTaskerModule.loadModule(container);
return container.get<UserLockedEmailTasker>(USER_LOCKED_EMAIL_TASKER_DI_TOKENS.USER_LOCKED_EMAIL_TASKER);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { UserLockedEmailTasker } from "@calcom/features/ee/api-keys/service/userLockedEmail/tasker/UserLockedEmailTasker";
import { USER_LOCKED_EMAIL_TASKER_DI_TOKENS } from "./tokens";
import { moduleLoader as userLockedEmailSyncTaskerModule } from "./UserLockedEmailSyncTasker.module";
import { moduleLoader as userLockedEmailTriggerTaskerModule } from "./UserLockedEmailTriggerDevTasker.module";

const thisModule = createModule();
const token = USER_LOCKED_EMAIL_TASKER_DI_TOKENS.USER_LOCKED_EMAIL_TASKER;
const moduleToken = USER_LOCKED_EMAIL_TASKER_DI_TOKENS.USER_LOCKED_EMAIL_TASKER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: UserLockedEmailTasker,
depsMap: {
logger: loggerServiceModule,
asyncTasker: userLockedEmailTriggerTaskerModule,
syncTasker: userLockedEmailSyncTaskerModule,
},
});

export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { UserLockedEmailTriggerDevTasker } from "@calcom/features/ee/api-keys/service/userLockedEmail/tasker/UserLockedEmailTriggerDevTasker";

import { USER_LOCKED_EMAIL_TASKER_DI_TOKENS } from "./tokens";

const thisModule = createModule();
const token = USER_LOCKED_EMAIL_TASKER_DI_TOKENS.USER_LOCKED_EMAIL_TRIGGER_TASKER;
const moduleToken = USER_LOCKED_EMAIL_TASKER_DI_TOKENS.USER_LOCKED_EMAIL_TRIGGER_TASKER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: UserLockedEmailTriggerDevTasker,
depsMap: {
logger: loggerServiceModule,
},
});

export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
8 changes: 8 additions & 0 deletions packages/features/ee/api-keys/di/tasker/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const USER_LOCKED_EMAIL_TASKER_DI_TOKENS = {
USER_LOCKED_EMAIL_TASKER: Symbol("UserLockedEmailTasker"),
USER_LOCKED_EMAIL_TASKER_MODULE: Symbol("UserLockedEmailTaskerModule"),
USER_LOCKED_EMAIL_SYNC_TASKER: Symbol("UserLockedEmailSyncTasker"),
USER_LOCKED_EMAIL_SYNC_TASKER_MODULE: Symbol("UserLockedEmailSyncTaskerModule"),
USER_LOCKED_EMAIL_TRIGGER_TASKER: Symbol("UserLockedEmailTriggerTasker"),
USER_LOCKED_EMAIL_TRIGGER_TASKER_MODULE: Symbol("UserLockedEmailTriggerTaskerModule"),
};
24 changes: 19 additions & 5 deletions packages/features/ee/api-keys/lib/autoLock.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import type { RatelimitResponse } from "@unkey/ratelimit";
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";

import process from "node:process";
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
import { RedisService } from "@calcom/features/redis/RedisService";
import prisma from "@calcom/prisma";

import type { RatelimitResponse } from "@unkey/ratelimit";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { handleAutoLock } from "./autoLock";

// Mock the dependencies
vi.mock("@calcom/features/redis/RedisService");
vi.mock("@calcom/features/ee/api-keys/lib/apiKeys", () => ({
hashAPIKey: vi.fn((key) => `hashed_${key}`),
}));
vi.mock("@calcom/features/ee/api-keys/di/tasker/UserLockedEmailTasker.container", () => ({
getUserLockedEmailTasker: vi.fn(() => ({
sendEmail: vi.fn().mockResolvedValue({ runId: "test-run-id" }),
})),
}));
vi.mock("@calcom/prisma", () => ({
default: {
user: {
Expand All @@ -34,7 +38,9 @@ describe("autoLock", () => {
beforeEach(() => {
// Reset all mocks before each test
vi.clearAllMocks();
vi.mocked(RedisService).mockImplementation(function() { return mockRedis as any; });
vi.mocked(RedisService).mockImplementation(function () {
return mockRedis as any;
});

// Mock environment variables
process.env.UPSTASH_REDIS_REST_TOKEN = "test-token";
Expand Down Expand Up @@ -129,6 +135,8 @@ describe("autoLock", () => {
id: true,
email: true,
username: true,
name: true,
locale: true,
},
});
expect(mockRedis.del).toHaveBeenCalledWith("autolock:email:[email protected]");
Expand Down Expand Up @@ -191,6 +199,8 @@ describe("autoLock", () => {
id: true,
email: true,
username: true,
name: true,
locale: true,
},
});
});
Expand Down Expand Up @@ -301,6 +311,8 @@ describe("autoLock", () => {
id: true,
email: true,
username: true,
name: true,
locale: true,
},
});
});
Expand Down Expand Up @@ -328,6 +340,8 @@ describe("autoLock", () => {
id: true,
email: true,
username: true,
name: true,
locale: true,
},
});
});
Expand Down
Loading
Loading