diff --git a/apps/web/pages/settings/admin/lockedSMS/UsersTable.tsx b/apps/web/pages/settings/admin/lockedSMS/UsersTable.tsx
new file mode 100644
index 0000000000..e0a639ac7d
--- /dev/null
+++ b/apps/web/pages/settings/admin/lockedSMS/UsersTable.tsx
@@ -0,0 +1,150 @@
+import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
+import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
+import { SMSLockState } from "@calcom/prisma/client";
+import { trpc } from "@calcom/trpc/react";
+import type { IconName } from "@calcom/ui";
+import { Avatar, DropdownActions, Table } from "@calcom/ui";
+
+const { Cell, ColumnTitle, Header, Row } = Table;
+
+type Props = {
+ setSMSLockState: (param: { userId?: number; teamId?: number; lock: boolean }) => void;
+};
+
+type User = {
+ id: number;
+ username: string | null;
+ name: string | null;
+ email: string;
+ smsLockState: SMSLockState;
+ avatarUrl: string | null;
+};
+
+type Team = {
+ id: number;
+ name: string;
+ smsLockState: SMSLockState;
+ slug: string | null;
+ logoUrl?: string | null;
+};
+
+function UsersTable({ setSMSLockState }: Props) {
+ const { data: usersAndTeams } = trpc.viewer.admin.getSMSLockStateTeamsUsers.useQuery();
+
+ if (!usersAndTeams) {
+ return <>>;
+ }
+
+ const users = usersAndTeams.users.locked.concat(usersAndTeams.users.reviewNeeded);
+ const teams = usersAndTeams.teams.locked.concat(usersAndTeams.teams.reviewNeeded);
+
+ return ;
+}
+
+const LockStatusTable = ({
+ users = [],
+ teams = [],
+ setSMSLockState,
+}: {
+ users?: User[];
+ teams?: Team[];
+ setSMSLockState: (param: { userId?: number; teamId?: number; lock: boolean }) => void;
+}) => {
+ function getActions({ user, team }: { user?: User; team?: Team }) {
+ const smsLockState = user?.smsLockState ?? team?.smsLockState;
+ if (!smsLockState) return [];
+
+ const actions = [
+ {
+ id: "unlock-sms",
+ label: smsLockState === SMSLockState.LOCKED ? "Unlock SMS sending" : "Lock SMS sending",
+ onClick: () =>
+ setSMSLockState({
+ userId: user ? user.id : undefined,
+ teamId: team ? team.id : undefined,
+ lock: smsLockState !== SMSLockState.LOCKED,
+ }),
+ icon: "lock" as IconName,
+ },
+ ];
+ if (smsLockState === SMSLockState.REVIEW_NEEDED) {
+ actions.push({
+ id: "reviewed",
+ label: "Mark as Reviewed",
+ onClick: () =>
+ setSMSLockState({
+ userId: user ? user.id : undefined,
+ teamId: team ? team.id : undefined,
+ lock: false,
+ }),
+ icon: "pencil" as IconName,
+ });
+ }
+
+ return actions;
+ }
+
+ return (
+ <>
+
+
+ User/Team
+ Status
+
+ Edit
+
+
+
+
+ {users.map((user) => (
+
+
+
+ {" "}
+
+
+ {user.name}
+ /{user.username}
+
+ {user.email}
+
+
+ |
+
+ {user.smsLockState} |
+
+
+ |
+
+ ))}
+ {teams.map((team) => (
+
+
+
+
+
+ {team.name}
+ /team/{team.slug}
+
+
+ |
+ {team.smsLockState} |
+
+
+ |
+
+ ))}
+
+
+ >
+ );
+};
+export default UsersTable;
diff --git a/apps/web/pages/settings/admin/lockedSMS/index.tsx b/apps/web/pages/settings/admin/lockedSMS/index.tsx
new file mode 100644
index 0000000000..bb9f9b6f6c
--- /dev/null
+++ b/apps/web/pages/settings/admin/lockedSMS/index.tsx
@@ -0,0 +1,13 @@
+"use client";
+
+import PageWrapper from "@components/PageWrapper";
+import { getLayout } from "@components/auth/layouts/AdminLayout";
+
+import LockedSMSView from "./lockedSMSView";
+
+const LockedSMSPage = () => ;
+
+LockedSMSPage.getLayout = getLayout;
+LockedSMSPage.PageWrapper = PageWrapper;
+
+export default LockedSMSPage;
diff --git a/apps/web/pages/settings/admin/lockedSMS/lockedSMSView.tsx b/apps/web/pages/settings/admin/lockedSMS/lockedSMSView.tsx
new file mode 100644
index 0000000000..079586abd6
--- /dev/null
+++ b/apps/web/pages/settings/admin/lockedSMS/lockedSMSView.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import UsersTable from "@pages/settings/admin/lockedSMS/UsersTable";
+import { useState } from "react";
+
+import { trpc } from "@calcom/trpc";
+import { Button, Meta, TextField, showToast } from "@calcom/ui";
+
+export default function LockedSMSView() {
+ const [username, setUsername] = useState("");
+ const [teamSlug, setTeamSlug] = useState("");
+
+ const utils = trpc.useContext();
+
+ const mutation = trpc.viewer.admin.setSMSLockState.useMutation({
+ onSuccess: (data) => {
+ if (data) {
+ showToast(`${data.name} successfully ${data.locked ? "locked" : "unlocked"}`, "success");
+ }
+ utils.viewer.admin.getSMSLockStateTeamsUsers.invalidate();
+ },
+ onError: (error) => {
+ showToast(`${error}`, "error");
+ utils.viewer.admin.getSMSLockStateTeamsUsers.invalidate();
+ },
+ });
+
+ function setSMSLockState({ userId, teamId, lock }: { userId?: number; teamId?: number; lock: boolean }) {
+ mutation.mutate({
+ userId,
+ teamId,
+ lock,
+ });
+ }
+
+ return (
+
+
+
+
+ setUsername(event.target.value)}
+ value={username}
+ />
+
+
+
+ {
+ setTeamSlug(event.target.value);
+ }}
+ value={teamSlug}
+ />
+
+
+
+
+
+ );
+}
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 91779164e3..bd79bbce90 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -2413,5 +2413,6 @@
"no_show_url_info":"The URL for No Show Feedback",
"no_support_needed":"No Support Needed?",
"hide_support":"Hide Support",
+ "lockedSMS": "Locked SMS",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts
index 3456170bc4..a368fea996 100644
--- a/packages/features/bookings/lib/handleCancelBooking.ts
+++ b/packages/features/bookings/lib/handleCancelBooking.ts
@@ -316,7 +316,6 @@ async function handler(req: CustomRequest) {
...{ eventType: { slug: bookingToDelete.eventType.slug } },
},
hideBranding: !!bookingToDelete.eventType.owner?.hideBranding,
- eventTypeRequiresConfirmation: bookingToDelete.eventType.requiresConfirmation,
});
}
diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts
index d2c0f394a3..fb85e1b319 100644
--- a/packages/features/bookings/lib/handleConfirmation.ts
+++ b/packages/features/bookings/lib/handleConfirmation.ts
@@ -288,7 +288,6 @@ export async function handleConfirmation(args: {
calendarEvent: evtOfBooking,
isFirstRecurringEvent: isFirstBooking,
hideBranding: !!updatedBookings[index].eventType?.owner?.hideBranding,
- eventTypeRequiresConfirmation: true,
});
}
} catch (error) {
diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts
index 21d7c929a5..8f5090ed8b 100644
--- a/packages/features/bookings/lib/handleNewBooking.ts
+++ b/packages/features/bookings/lib/handleNewBooking.ts
@@ -2354,7 +2354,6 @@ async function handler(
isFirstRecurringEvent: true,
hideBranding: !!eventType.owner?.hideBranding,
seatReferenceUid: evt.attendeeSeatId,
- eventTypeRequiresConfirmation: eventType.requiresConfirmation,
});
} catch (error) {
loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error }));
diff --git a/packages/features/bookings/lib/handleSeats/handleSeats.ts b/packages/features/bookings/lib/handleSeats/handleSeats.ts
index ae96d4ab4f..81d8e62fd2 100644
--- a/packages/features/bookings/lib/handleSeats/handleSeats.ts
+++ b/packages/features/bookings/lib/handleSeats/handleSeats.ts
@@ -110,7 +110,6 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => {
isFirstRecurringEvent: true,
emailAttendeeSendToOverride: bookerEmail,
seatReferenceUid: evt.attendeeSeatId,
- eventTypeRequiresConfirmation: eventType.requiresConfirmation,
});
} catch (error) {
loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error }));
diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
index 3faf361ce7..299d9bac63 100644
--- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
@@ -56,6 +56,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!reminder.workflowStep || !reminder.booking) {
continue;
}
+ const userId = reminder.workflowStep.workflow.userId;
+ const teamId = reminder.workflowStep.workflow.teamId;
+
try {
const sendTo =
reminder.workflowStep.action === WorkflowActions.SMS_NUMBER
@@ -141,17 +144,26 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
if (message?.length && message?.length > 0 && sendTo) {
- const scheduledSMS = await twilio.scheduleSMS(sendTo, message, reminder.scheduledDate, senderID);
+ const scheduledSMS = await twilio.scheduleSMS(
+ sendTo,
+ message,
+ reminder.scheduledDate,
+ senderID,
+ userId,
+ teamId
+ );
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- scheduled: true,
- referenceId: scheduledSMS.sid,
- },
- });
+ if (scheduledSMS) {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ scheduled: true,
+ referenceId: scheduledSMS.sid,
+ },
+ });
+ }
}
} catch (error) {
console.log(`Error scheduling SMS with error ${error}`);
diff --git a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts
index c4021c4712..5660407715 100644
--- a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts
@@ -8,6 +8,8 @@ import prisma from "@calcom/prisma";
import { WorkflowActions, WorkflowMethods } from "@calcom/prisma/enums";
import { getWhatsappTemplateFunction } from "../lib/actionHelperFunctions";
+import type { PartialWorkflowReminder } from "../lib/getWorkflowReminders";
+import { select } from "../lib/getWorkflowReminders";
import * as twilio from "../lib/reminders/providers/twilioProvider";
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -28,7 +30,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});
//find all unscheduled WHATSAPP reminders
- const unscheduledReminders = await prisma.workflowReminder.findMany({
+ const unscheduledReminders = (await prisma.workflowReminder.findMany({
where: {
method: WorkflowMethods.WHATSAPP,
scheduled: false,
@@ -36,17 +38,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
lte: dayjs().add(7, "day").toISOString(),
},
},
- include: {
- workflowStep: true,
- booking: {
- include: {
- eventType: true,
- user: true,
- attendees: true,
- },
- },
- },
- });
+ select,
+ })) as PartialWorkflowReminder[];
if (!unscheduledReminders.length) {
res.json({ ok: true });
@@ -57,6 +50,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!reminder.workflowStep || !reminder.booking) {
continue;
}
+ const userId = reminder.workflowStep.workflow.userId;
+ const teamId = reminder.workflowStep.workflow.teamId;
+
try {
const sendTo =
reminder.workflowStep.action === WorkflowActions.WHATSAPP_NUMBER
@@ -91,17 +87,27 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
);
if (message?.length && message?.length > 0 && sendTo) {
- const scheduledSMS = await twilio.scheduleSMS(sendTo, message, reminder.scheduledDate, "", true);
+ const scheduledSMS = await twilio.scheduleSMS(
+ sendTo,
+ message,
+ reminder.scheduledDate,
+ "",
+ userId,
+ teamId,
+ true
+ );
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- scheduled: true,
- referenceId: scheduledSMS.sid,
- },
- });
+ if (scheduledSMS) {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ scheduled: true,
+ referenceId: scheduledSMS.sid,
+ },
+ });
+ }
}
} catch (error) {
console.log(`Error scheduling WHATSAPP with error ${error}`);
diff --git a/packages/features/ee/workflows/components/AddActionDialog.tsx b/packages/features/ee/workflows/components/AddActionDialog.tsx
index e1f492abab..9af66441c0 100644
--- a/packages/features/ee/workflows/components/AddActionDialog.tsx
+++ b/packages/features/ee/workflows/components/AddActionDialog.tsx
@@ -175,8 +175,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
label: string;
value: WorkflowActions;
needsTeamsUpgrade: boolean;
- needsOrgsUpgrade: boolean;
- }) => option.needsTeamsUpgrade || option.needsOrgsUpgrade}
+ }) => option.needsTeamsUpgrade}
/>
);
}}
diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx
index c643725171..f45ec61ed7 100644
--- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx
+++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx
@@ -5,7 +5,6 @@ import { useFormContext } from "react-hook-form";
import { Trans } from "react-i18next";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
-import { isTextMessageToAttendeeAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -105,12 +104,6 @@ const WorkflowListItem = (props: ItemProps) => {
}
});
- const needsRequiresConfirmationWarning =
- !eventType.requiresConfirmation &&
- workflow.steps.find((step) => {
- return isTextMessageToAttendeeAction(step.action);
- });
-
return (
@@ -179,15 +172,6 @@ const WorkflowListItem = (props: ItemProps) => {
-
- {needsRequiresConfirmationWarning ? (
-
-
-
{t("requires_confirmation_mandatory")}
-
- ) : (
- <>>
- )}
);
};
diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
index 75ae7ac75b..db99627dad 100644
--- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
+++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
@@ -45,7 +45,6 @@ import {
isAttendeeAction,
isSMSAction,
isSMSOrWhatsappAction,
- isTextMessageToAttendeeAction,
isWhatsappAction,
} from "../lib/actionHelperFunctions";
import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants";
@@ -120,10 +119,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
form.getValues("trigger") === WorkflowTriggerEvents.AFTER_EVENT
);
- const [isRequiresConfirmationNeeded, setIsRequiresConfirmationNeeded] = useState(
- isTextMessageToAttendeeAction(step?.action)
- );
-
const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery();
const triggerOptions = getWorkflowTriggerOptions(t);
const templateOptions = getWorkflowTemplateOptions(t, step?.action);
@@ -345,7 +340,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
value: step.action,
needsTeamsUpgrade: false,
- needsOrgsUpgrade: false,
};
const selectedTemplate = { label: t(`${step.template.toLowerCase()}`), value: step.template };
@@ -463,12 +457,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
setIsEmailSubjectNeeded(true);
}
- if (isTextMessageToAttendeeAction(val.value)) {
- setIsRequiresConfirmationNeeded(true);
- } else {
- setIsRequiresConfirmationNeeded(false);
- }
-
if (
form.getValues(`steps.${step.stepNumber - 1}.template`) ===
WorkflowTemplates.REMINDER
@@ -541,20 +529,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
label: string;
value: WorkflowActions;
needsTeamsUpgrade: boolean;
- needsOrgsUpgrade: boolean;
- }) => option.needsTeamsUpgrade || option.needsOrgsUpgrade}
+ }) => option.needsTeamsUpgrade}
/>
);
}}
/>
- {isRequiresConfirmationNeeded ? (
-
-
-
{t("requires_confirmation_mandatory")}
-
- ) : (
- <>>
- )}
{isPhoneNumberNeeded && (
diff --git a/packages/features/ee/workflows/lib/actionHelperFunctions.ts b/packages/features/ee/workflows/lib/actionHelperFunctions.ts
index b66f2b7734..18b126bec3 100644
--- a/packages/features/ee/workflows/lib/actionHelperFunctions.ts
+++ b/packages/features/ee/workflows/lib/actionHelperFunctions.ts
@@ -42,9 +42,6 @@ export function isEmailToAttendeeAction(action: WorkflowActions) {
return action === WorkflowActions.EMAIL_ATTENDEE;
}
-export function isTextMessageToAttendeeAction(action?: WorkflowActions) {
- return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE;
-}
export function isTextMessageToSpecificNumber(action?: WorkflowActions) {
return action === WorkflowActions.SMS_NUMBER || action === WorkflowActions.WHATSAPP_NUMBER;
}
@@ -65,7 +62,7 @@ export function getWhatsappTemplateForTrigger(trigger: WorkflowTriggerEvents): W
}
}
-export function getWhatsappTemplateFunction(template: WorkflowTemplates): typeof whatsappReminderTemplate {
+export function getWhatsappTemplateFunction(template?: WorkflowTemplates): typeof whatsappReminderTemplate {
switch (template) {
case "CANCELLED":
return whatsappEventCancelledTemplate;
diff --git a/packages/features/ee/workflows/lib/getOptions.ts b/packages/features/ee/workflows/lib/getOptions.ts
index a1d80149ec..7b4cec8d49 100644
--- a/packages/features/ee/workflows/lib/getOptions.ts
+++ b/packages/features/ee/workflows/lib/getOptions.ts
@@ -2,12 +2,7 @@ import type { TFunction } from "next-i18next";
import { WorkflowActions } from "@calcom/prisma/enums";
-import {
- isTextMessageToAttendeeAction,
- isSMSOrWhatsappAction,
- isWhatsappAction,
- isEmailToAttendeeAction,
-} from "./actionHelperFunctions";
+import { isSMSOrWhatsappAction, isWhatsappAction, isEmailToAttendeeAction } from "./actionHelperFunctions";
import {
TIME_UNIT,
WHATSAPP_WORKFLOW_TEMPLATES,
@@ -25,9 +20,7 @@ export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean, is
return {
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
value: action,
- needsTeamsUpgrade:
- isSMSOrWhatsappAction(action) && !isTextMessageToAttendeeAction(action) && !isTeamsPlan,
- needsOrgsUpgrade: isTextMessageToAttendeeAction(action) && !isOrgsPlan,
+ needsTeamsUpgrade: isSMSOrWhatsappAction(action) && !isTeamsPlan,
};
});
}
diff --git a/packages/features/ee/workflows/lib/getWorkflowReminders.ts b/packages/features/ee/workflows/lib/getWorkflowReminders.ts
index 4c88206dbd..ea07e4cafa 100644
--- a/packages/features/ee/workflows/lib/getWorkflowReminders.ts
+++ b/packages/features/ee/workflows/lib/getWorkflowReminders.ts
@@ -3,7 +3,9 @@ import prisma from "@calcom/prisma";
import type { EventType, Prisma, User, WorkflowReminder, WorkflowStep } from "@calcom/prisma/client";
import { WorkflowMethods } from "@calcom/prisma/enums";
-type PartialWorkflowStep = Partial | null;
+type PartialWorkflowStep =
+ | (Partial & { workflow: { userId?: number; teamId?: number } })
+ | null;
type Booking = Prisma.BookingGetPayload<{
include: {
@@ -120,6 +122,12 @@ export const select: Prisma.WorkflowReminderSelect = {
template: true,
sender: true,
includeCalendarEvent: true,
+ workflow: {
+ select: {
+ userId: true,
+ teamId: true,
+ },
+ },
},
},
booking: {
diff --git a/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts b/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts
index d5eb0d1e2a..1f136929dd 100644
--- a/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts
+++ b/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts
@@ -1,10 +1,17 @@
import TwilioClient from "twilio";
+import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
+import logger from "@calcom/lib/logger";
+import prisma from "@calcom/prisma";
+import { SMSLockState } from "@calcom/prisma/enums";
+
declare global {
// eslint-disable-next-line no-var
var twilio: TwilioClient.Twilio | undefined;
}
+const log = logger.getSubLogger({ prefix: ["[twilioProvider]"] });
+
export const twilio =
globalThis.twilio ||
(process.env.TWILIO_SID && process.env.TWILIO_TOKEN && process.env.TWILIO_MESSAGING_SID)
@@ -31,8 +38,30 @@ function getSMSNumber(phone: string, whatsapp = false) {
return whatsapp ? `whatsapp:${phone}` : phone;
}
-export const sendSMS = async (phoneNumber: string, body: string, sender: string, whatsapp = false) => {
+export const sendSMS = async (
+ phoneNumber: string,
+ body: string,
+ sender: string,
+ userId?: number | null,
+ teamId?: number | null,
+ whatsapp = false
+) => {
assertTwilio(twilio);
+
+ const isSMSSendingLocked = await isLockedForSMSSending(userId, teamId);
+
+ if (isSMSSendingLocked) {
+ log.debug(`${teamId ? `Team id ${teamId} ` : `User id ${userId} `} is locked for SMS sending `);
+ return;
+ }
+
+ if (!teamId && userId) {
+ await checkSMSRateLimit({
+ identifier: `sms:user:${userId}`,
+ rateLimitingType: "smsMonth",
+ });
+ }
+
const response = await twilio.messages.create({
body: body,
messagingServiceSid: process.env.TWILIO_MESSAGING_SID,
@@ -48,9 +77,26 @@ export const scheduleSMS = async (
body: string,
scheduledDate: Date,
sender: string,
+ userId?: number | null,
+ teamId?: number | null,
whatsapp = false
) => {
assertTwilio(twilio);
+
+ const isSMSSendingLocked = await isLockedForSMSSending(userId, teamId);
+
+ if (isSMSSendingLocked) {
+ log.debug(`${teamId ? `Team id ${teamId} ` : `User id ${userId} `} is locked for SMS sending `);
+ return;
+ }
+
+ if (!teamId && userId) {
+ await checkSMSRateLimit({
+ identifier: `sms:user:${userId}`,
+ rateLimitingType: "smsMonth",
+ });
+ }
+
const response = await twilio.messages.create({
body: body,
messagingServiceSid: process.env.TWILIO_MESSAGING_SID,
@@ -90,3 +136,44 @@ export const verifyNumber = async (phoneNumber: string, code: string) => {
}
}
};
+
+async function isLockedForSMSSending(userId?: number | null, teamId?: number | null) {
+ if (teamId) {
+ const team = await prisma.team.findFirst({
+ where: {
+ id: teamId,
+ },
+ });
+ return team?.smsLockState === SMSLockState.LOCKED;
+ }
+
+ if (userId) {
+ const memberships = await prisma.membership.findMany({
+ where: {
+ userId: userId,
+ },
+ select: {
+ team: {
+ select: {
+ smsLockState: true,
+ },
+ },
+ },
+ });
+
+ const memberOfLockedTeam = memberships.find(
+ (membership) => membership.team.smsLockState === SMSLockState.LOCKED
+ );
+
+ if (!!memberOfLockedTeam) {
+ return true;
+ }
+
+ const user = await prisma.user.findFirst({
+ where: {
+ id: userId,
+ },
+ });
+ return user?.smsLockState === SMSLockState.LOCKED;
+ }
+}
diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts
index c23291ab94..68422b62cc 100644
--- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts
+++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts
@@ -1,10 +1,7 @@
import type { Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
-import {
- isSMSAction,
- isTextMessageToAttendeeAction,
- isWhatsappAction,
-} from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
+import { isSMSAction, isWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
+import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
import { SENDER_NAME } from "@calcom/lib/constants";
import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
@@ -25,7 +22,6 @@ type ProcessWorkflowStepParams = {
emailAttendeeSendToOverride?: string;
hideBranding?: boolean;
seatReferenceUid?: string;
- eventTypeRequiresConfirmation?: boolean;
};
export interface ScheduleWorkflowRemindersArgs extends ProcessWorkflowStepParams {
@@ -48,12 +44,14 @@ const processWorkflowStep = async (
emailAttendeeSendToOverride,
hideBranding,
seatReferenceUid,
- eventTypeRequiresConfirmation,
}: ProcessWorkflowStepParams
) => {
- if (isTextMessageToAttendeeAction(step.action) && !eventTypeRequiresConfirmation) return;
-
if (isSMSAction(step.action)) {
+ await checkSMSRateLimit({
+ identifier: `sms:${workflow.teamId ? "team:" : "user:"}${workflow.teamId || workflow.userId}`,
+ rateLimitingType: "sms",
+ });
+
const sendTo = step.action === WorkflowActions.SMS_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleSMSReminder({
evt,
@@ -141,7 +139,6 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
emailAttendeeSendToOverride = "",
hideBranding,
seatReferenceUid,
- eventTypeRequiresConfirmation = false,
} = args;
if (isNotConfirmed || !workflows.length) return;
@@ -173,7 +170,6 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
smsReminderNumber,
hideBranding,
seatReferenceUid,
- eventTypeRequiresConfirmation,
});
}
}
@@ -204,11 +200,10 @@ export interface SendCancelledRemindersArgs {
smsReminderNumber: string | null;
evt: ExtendedCalendarEvent;
hideBranding?: boolean;
- eventTypeRequiresConfirmation?: boolean;
}
export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) => {
- const { workflows, smsReminderNumber, evt, hideBranding, eventTypeRequiresConfirmation } = args;
+ const { workflows, smsReminderNumber, evt, hideBranding } = args;
if (!workflows.length) return;
for (const workflowRef of workflows) {
@@ -221,7 +216,6 @@ export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) =
smsReminderNumber,
hideBranding,
calendarEvent: evt,
- eventTypeRequiresConfirmation,
});
}
}
diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
index a555d74dff..f47cb12586 100644
--- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
+++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
@@ -86,6 +86,7 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
isVerificationPending = false,
seatReferenceUid,
} = args;
+
const { startTime, endTime } = evt;
const uid = evt.uid as string;
const currentDate = dayjs();
@@ -184,7 +185,7 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT
) {
try {
- await twilio.sendSMS(reminderPhone, smsMessage, senderID);
+ await twilio.sendSMS(reminderPhone, smsMessage, senderID, userId, teamId);
} catch (error) {
log.error(`Error sending SMS with error ${error}`);
}
@@ -203,20 +204,24 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
reminderPhone,
smsMessage,
scheduledDate.toDate(),
- senderID
+ senderID,
+ userId,
+ teamId
);
- await prisma.workflowReminder.create({
- data: {
- bookingUid: uid,
- workflowStepId: workflowStepId,
- method: WorkflowMethods.SMS,
- scheduledDate: scheduledDate.toDate(),
- scheduled: true,
- referenceId: scheduledSMS.sid,
- seatReferenceId: seatReferenceUid,
- },
- });
+ if (scheduledSMS) {
+ await prisma.workflowReminder.create({
+ data: {
+ bookingUid: uid,
+ workflowStepId: workflowStepId,
+ method: WorkflowMethods.SMS,
+ scheduledDate: scheduledDate.toDate(),
+ scheduled: true,
+ referenceId: scheduledSMS.sid,
+ seatReferenceId: seatReferenceUid,
+ },
+ });
+ }
} catch (error) {
log.error(`Error scheduling SMS with error ${error}`);
}
diff --git a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts
index 6652924898..967b8a56af 100644
--- a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts
+++ b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts
@@ -35,6 +35,7 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) =
isVerificationPending = false,
seatReferenceUid,
} = args;
+
const { startTime, endTime } = evt;
const uid = evt.uid as string;
const currentDate = dayjs();
@@ -147,7 +148,7 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) =
triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT
) {
try {
- await twilio.sendSMS(reminderPhone, textMessage, "", true);
+ await twilio.sendSMS(reminderPhone, textMessage, "", userId, teamId, true);
} catch (error) {
console.log(`Error sending WHATSAPP with error ${error}`);
}
@@ -167,20 +168,24 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) =
textMessage,
scheduledDate.toDate(),
"",
+ userId,
+ teamId,
true
);
- await prisma.workflowReminder.create({
- data: {
- bookingUid: uid,
- workflowStepId: workflowStepId,
- method: WorkflowMethods.WHATSAPP,
- scheduledDate: scheduledDate.toDate(),
- scheduled: true,
- referenceId: scheduledWHATSAPP.sid,
- seatReferenceId: seatReferenceUid,
- },
- });
+ if (scheduledWHATSAPP) {
+ await prisma.workflowReminder.create({
+ data: {
+ bookingUid: uid,
+ workflowStepId: workflowStepId,
+ method: WorkflowMethods.WHATSAPP,
+ scheduledDate: scheduledDate.toDate(),
+ scheduled: true,
+ referenceId: scheduledWHATSAPP.sid,
+ seatReferenceId: seatReferenceUid,
+ },
+ });
+ }
} catch (error) {
console.log(`Error scheduling WHATSAPP with error ${error}`);
}
diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx
index 329a4efda4..2281735209 100644
--- a/packages/features/settings/layouts/SettingsLayout.tsx
+++ b/packages/features/settings/layouts/SettingsLayout.tsx
@@ -119,6 +119,7 @@ const tabs: VerticalTabItemProps[] = [
{ name: "apps", href: "/settings/admin/apps/calendar" },
{ name: "users", href: "/settings/admin/users" },
{ name: "organizations", href: "/settings/admin/organizations" },
+ { name: "lockedSMS", href: "/settings/admin/lockedSMS" },
{ name: "oAuth", href: "/settings/admin/oAuth" },
],
},
diff --git a/packages/features/settings/layouts/SettingsLayoutAppDir.tsx b/packages/features/settings/layouts/SettingsLayoutAppDir.tsx
index 6f9c454bd5..c62e1bcc5d 100644
--- a/packages/features/settings/layouts/SettingsLayoutAppDir.tsx
+++ b/packages/features/settings/layouts/SettingsLayoutAppDir.tsx
@@ -107,6 +107,7 @@ const tabs: VerticalTabItemProps[] = [
{ name: "apps", href: "/settings/admin/apps/calendar" },
{ name: "users", href: "/settings/admin/users" },
{ name: "organizations", href: "/settings/admin/organizations" },
+ { name: "lockedSMS", href: "/settings/admin/lockedSMS" },
{ name: "oAuth", href: "/settings/admin/oAuth" },
],
},
diff --git a/packages/lib/checkRateLimitAndThrowError.ts b/packages/lib/checkRateLimitAndThrowError.ts
index d88845203f..64fd6189a6 100644
--- a/packages/lib/checkRateLimitAndThrowError.ts
+++ b/packages/lib/checkRateLimitAndThrowError.ts
@@ -1,3 +1,5 @@
+import prisma from "@calcom/prisma";
+import { SMSLockState } from "@calcom/prisma/enums";
import { TRPCError } from "@calcom/trpc/server";
import type { RateLimitHelper } from "./rateLimit";
@@ -23,3 +25,55 @@ export async function checkRateLimitAndThrowError({
});
}
}
+
+export async function checkSMSRateLimit({
+ rateLimitingType = "sms",
+ identifier,
+ onRateLimiterResponse,
+ opts,
+}: RateLimitHelper) {
+ const response = await rateLimiter()({ rateLimitingType, identifier, opts });
+ const { success } = response;
+
+ if (onRateLimiterResponse) onRateLimiterResponse(response);
+
+ if (!success) {
+ await changeSMSLockState(
+ identifier,
+ rateLimitingType === "sms" ? SMSLockState.LOCKED : SMSLockState.REVIEW_NEEDED
+ );
+ }
+}
+
+async function changeSMSLockState(identifier: string, status: SMSLockState) {
+ let userId, teamId;
+
+ if (identifier.startsWith("sms:user:")) {
+ userId = Number(identifier.slice(9));
+ } else if (identifier.startsWith("sms:team:")) {
+ teamId = Number(identifier.slice(9));
+ }
+
+ if (userId) {
+ await prisma.user.update({
+ where: {
+ id: userId,
+ profiles: { none: {} },
+ },
+ data: {
+ smsLockState: status,
+ },
+ });
+ } else {
+ await prisma.team.update({
+ where: {
+ id: teamId,
+ parentId: null,
+ isOrganization: false,
+ },
+ data: {
+ smsLockState: status,
+ },
+ });
+ }
+}
diff --git a/packages/lib/rateLimit.ts b/packages/lib/rateLimit.ts
index 1aab25284e..6c2c8f28a6 100644
--- a/packages/lib/rateLimit.ts
+++ b/packages/lib/rateLimit.ts
@@ -6,7 +6,7 @@ import logger from "./logger";
const log = logger.getSubLogger({ prefix: ["RateLimit"] });
export type RateLimitHelper = {
- rateLimitingType?: "core" | "forcedSlowMode" | "common" | "api" | "ai";
+ rateLimitingType?: "core" | "forcedSlowMode" | "common" | "api" | "ai" | "sms" | "smsMonth";
identifier: string;
opts?: LimitOptions;
/**
@@ -80,6 +80,22 @@ export function rateLimiter() {
async: true,
timeout,
}),
+ sms: new Ratelimit({
+ rootKey: UNKEY_ROOT_KEY,
+ namespace: "sms",
+ limit: 50,
+ duration: "5m",
+ async: true,
+ timeout,
+ }),
+ smsMonth: new Ratelimit({
+ rootKey: UNKEY_ROOT_KEY,
+ namespace: "smsMonth",
+ limit: 250,
+ duration: "30d",
+ async: true,
+ timeout,
+ }),
};
async function rateLimit({ rateLimitingType = "core", identifier, opts }: RateLimitHelper) {
diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts
index 118f069a90..7f8fb00dbf 100644
--- a/packages/lib/test/builder.ts
+++ b/packages/lib/test/builder.ts
@@ -239,6 +239,7 @@ type UserPayload = Prisma.UserGetPayload<{
receiveMonthlyDigestEmail: true;
movedToProfileId: true;
isPlatformManaged: true;
+ smsLockState: true;
};
}>;
export const buildUser = >(
@@ -246,6 +247,7 @@ export const buildUser = >(
): UserPayload & { priority: number | null } => {
return {
locked: false,
+ smsLockState: "UNLOCKED",
name: faker.name.firstName(),
email: faker.internet.email(),
timeZone: faker.address.timeZone(),
diff --git a/packages/prisma/migrations/20240417175106_add_sms_lock_state/migration.sql b/packages/prisma/migrations/20240417175106_add_sms_lock_state/migration.sql
new file mode 100644
index 0000000000..fc85d63833
--- /dev/null
+++ b/packages/prisma/migrations/20240417175106_add_sms_lock_state/migration.sql
@@ -0,0 +1,8 @@
+-- CreateEnum
+CREATE TYPE "SMSLockState" AS ENUM ('LOCKED', 'UNLOCKED', 'REVIEW_NEEDED');
+
+-- AlterTable
+ALTER TABLE "Team" ADD COLUMN "smsLockState" "SMSLockState" NOT NULL DEFAULT 'UNLOCKED';
+
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "smsLockState" "SMSLockState" NOT NULL DEFAULT 'UNLOCKED';
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 2c1c38d010..12198062b6 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -318,6 +318,7 @@ model User {
isPlatformManaged Boolean @default(false)
OutOfOfficeReasons OutOfOfficeReason[]
+ smsLockState SMSLockState @default(UNLOCKED)
@@unique([email])
@@unique([email, username])
@@ -398,6 +399,7 @@ model Team {
dsyncTeamGroupMapping DSyncTeamGroupMapping[]
isPlatform Boolean @default(false)
platformOAuthClient PlatformOAuthClient[]
+ smsLockState SMSLockState @default(UNLOCKED)
@@unique([slug, parentId])
@@index([parentId])
@@ -1346,3 +1348,9 @@ model Task {
maxAttempts Int @default(3)
lastError String?
}
+
+enum SMSLockState {
+ LOCKED
+ UNLOCKED
+ REVIEW_NEEDED
+}
diff --git a/packages/trpc/server/routers/viewer/admin/_router.ts b/packages/trpc/server/routers/viewer/admin/_router.ts
index 9bb381d29c..c81693b56f 100644
--- a/packages/trpc/server/routers/viewer/admin/_router.ts
+++ b/packages/trpc/server/routers/viewer/admin/_router.ts
@@ -6,6 +6,7 @@ import { ZListMembersSchema } from "./listPaginated.schema";
import { ZAdminLockUserAccountSchema } from "./lockUserAccount.schema";
import { ZAdminRemoveTwoFactor } from "./removeTwoFactor.schema";
import { ZAdminPasswordResetSchema } from "./sendPasswordReset.schema";
+import { ZSetSMSLockState } from "./setSMSLockState.schema";
const NAMESPACE = "admin";
@@ -47,4 +48,18 @@ export const adminRouter = router({
);
return handler(opts);
}),
+ getSMSLockStateTeamsUsers: authedAdminProcedure.query(async (opts) => {
+ const handler = await importHandler(
+ namespaced("getSMSLockStateTeamsUsers"),
+ () => import("./getSMSLockStateTeamsUsers.handler")
+ );
+ return handler(opts);
+ }),
+ setSMSLockState: authedAdminProcedure.input(ZSetSMSLockState).mutation(async (opts) => {
+ const handler = await importHandler(
+ namespaced("setSMSLockState"),
+ () => import("./setSMSLockState.handler")
+ );
+ return handler(opts);
+ }),
});
diff --git a/packages/trpc/server/routers/viewer/admin/getSMSLockStateTeamsUsers.handler.ts b/packages/trpc/server/routers/viewer/admin/getSMSLockStateTeamsUsers.handler.ts
new file mode 100644
index 0000000000..26606576e1
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/admin/getSMSLockStateTeamsUsers.handler.ts
@@ -0,0 +1,71 @@
+import { prisma } from "@calcom/prisma";
+import { SMSLockState } from "@calcom/prisma/client";
+
+import type { TrpcSessionUser } from "../../../trpc";
+
+type GetOptions = {
+ ctx: {
+ user: NonNullable;
+ };
+};
+
+const getSMSLockStateTeamsUsers = async ({ ctx }: GetOptions) => {
+ const userSelect = {
+ id: true,
+ smsLockState: true,
+ email: true,
+ username: true,
+ name: true,
+ avatarUrl: true,
+ };
+
+ const teamSelect = {
+ id: true,
+ smsLockState: true,
+ slug: true,
+ name: true,
+ logoUrl: true,
+ };
+
+ const lockedUsers = await prisma.user.findMany({
+ where: {
+ smsLockState: SMSLockState.LOCKED,
+ },
+ select: userSelect,
+ });
+ const reviewNeededUsers = await prisma.user.findMany({
+ where: {
+ smsLockState: SMSLockState.REVIEW_NEEDED,
+ },
+ select: userSelect,
+ });
+
+ const lockedTeams = await prisma.team.findMany({
+ where: {
+ smsLockState: SMSLockState.LOCKED,
+ },
+ select: teamSelect,
+ });
+
+ const reviewNeededTeams = await prisma.team.findMany({
+ where: {
+ smsLockState: SMSLockState.REVIEW_NEEDED,
+ },
+ select: teamSelect,
+ });
+
+ const resultObj = {
+ users: {
+ locked: lockedUsers,
+ reviewNeeded: reviewNeededUsers,
+ },
+ teams: {
+ locked: lockedTeams,
+ reviewNeeded: reviewNeededTeams,
+ },
+ };
+
+ return resultObj;
+};
+
+export default getSMSLockStateTeamsUsers;
diff --git a/packages/trpc/server/routers/viewer/admin/getSMSLockStateTeamsUsers.schema.ts b/packages/trpc/server/routers/viewer/admin/getSMSLockStateTeamsUsers.schema.ts
new file mode 100644
index 0000000000..cb0ff5c3b5
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/admin/getSMSLockStateTeamsUsers.schema.ts
@@ -0,0 +1 @@
+export {};
diff --git a/packages/trpc/server/routers/viewer/admin/setSMSLockState.handler.ts b/packages/trpc/server/routers/viewer/admin/setSMSLockState.handler.ts
new file mode 100644
index 0000000000..a5279cf433
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/admin/setSMSLockState.handler.ts
@@ -0,0 +1,90 @@
+import { prisma } from "@calcom/prisma";
+import { SMSLockState } from "@calcom/prisma/client";
+
+import { TRPCError } from "@trpc/server";
+
+import type { TrpcSessionUser } from "../../../trpc";
+import type { TSetSMSLockState } from "./setSMSLockState.schema";
+
+type GetOptions = {
+ ctx: {
+ user: NonNullable;
+ };
+ input: TSetSMSLockState;
+};
+
+const setSMSLockState = async ({ input }: GetOptions) => {
+ const { userId, username, teamId, teamSlug, lock } = input;
+ if (userId) {
+ const updatedUser = await prisma.user.update({
+ where: {
+ id: userId,
+ },
+ data: {
+ smsLockState: lock ? SMSLockState.LOCKED : SMSLockState.UNLOCKED,
+ },
+ });
+ if (!updatedUser) {
+ throw new TRPCError({ code: "BAD_REQUEST", message: "User not found" });
+ }
+ return { name: updatedUser.username, locked: lock };
+ } else if (username) {
+ const userToUpdate = await prisma.user.findFirst({
+ where: {
+ username,
+ profiles: { none: {} },
+ },
+ });
+ if (!userToUpdate) {
+ throw new TRPCError({ code: "BAD_REQUEST", message: "User not found" });
+ }
+ if (userToUpdate) {
+ const updatedUser = await prisma.user.update({
+ where: {
+ id: userToUpdate.id,
+ },
+ data: {
+ smsLockState: lock ? SMSLockState.LOCKED : SMSLockState.UNLOCKED,
+ },
+ });
+ return { name: updatedUser.username, locked: lock };
+ }
+ } else if (teamId) {
+ const updatedTeam = await prisma.team.update({
+ where: {
+ id: teamId,
+ },
+ data: {
+ smsLockState: lock ? SMSLockState.LOCKED : SMSLockState.UNLOCKED,
+ },
+ });
+ if (!updatedTeam) {
+ throw new TRPCError({ code: "BAD_REQUEST", message: "Team not found" });
+ }
+ return { name: updatedTeam.slug, locked: lock };
+ } else if (teamSlug) {
+ const teamToUpdate = await prisma.team.findFirst({
+ where: {
+ slug: teamSlug,
+ parentId: null,
+ },
+ });
+ if (!teamToUpdate) {
+ throw new TRPCError({ code: "BAD_REQUEST", message: "Team not found" });
+ }
+ if (teamToUpdate) {
+ const updatedTeam = await prisma.team.update({
+ where: {
+ id: teamToUpdate.id,
+ },
+ data: {
+ smsLockState: lock ? SMSLockState.LOCKED : SMSLockState.UNLOCKED,
+ },
+ });
+ return { name: updatedTeam.slug, locked: lock };
+ }
+ }
+ throw new TRPCError({ code: "BAD_REQUEST", message: "Input data missing" });
+};
+
+export default setSMSLockState;
diff --git a/packages/trpc/server/routers/viewer/admin/setSMSLockState.schema.ts b/packages/trpc/server/routers/viewer/admin/setSMSLockState.schema.ts
new file mode 100644
index 0000000000..d51d5a1be1
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/admin/setSMSLockState.schema.ts
@@ -0,0 +1,11 @@
+import { z } from "zod";
+
+export const ZSetSMSLockState = z.object({
+ userId: z.number().optional(),
+ username: z.string().optional(),
+ teamId: z.number().optional(),
+ teamSlug: z.string().optional(),
+ lock: z.boolean().optional(),
+});
+
+export type TSetSMSLockState = z.infer;
diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts
index f729f89bf0..331b6c9ca3 100644
--- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts
+++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts
@@ -1,10 +1,6 @@
import type { Prisma } from "@prisma/client";
-import {
- isSMSOrWhatsappAction,
- isTextMessageToAttendeeAction,
- isTextMessageToSpecificNumber,
-} from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
+import { isSMSOrWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import {
deleteScheduledEmailReminder,
scheduleEmailReminder,
@@ -84,8 +80,6 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}
const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan;
- const hasOrgsPlan = IS_SELF_HOSTED || (ctx.user.profile?.organizationId ?? null);
-
const where: Prisma.EventTypeWhereInput = {};
where.id = {
in: activeOn,
@@ -427,21 +421,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
//step was edited
} else if (JSON.stringify(oldStep) !== JSON.stringify(newStep)) {
// check if step that require team plan already existed before
- if (
- !hasPaidPlan &&
- !isTextMessageToSpecificNumber(oldStep.action) &&
- isTextMessageToSpecificNumber(newStep.action)
- ) {
+ if (!hasPaidPlan && !isSMSOrWhatsappAction(oldStep.action) && isSMSOrWhatsappAction(newStep.action)) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not available on free plan" });
}
- // check if step that require org already existed before
- if (
- !hasOrgsPlan &&
- !isTextMessageToAttendeeAction(oldStep.action) &&
- isTextMessageToAttendeeAction(newStep.action)
- ) {
- throw new TRPCError({ code: "UNAUTHORIZED", message: "Enterprise plan required" });
- }
const requiresSender =
newStep.action === WorkflowActions.SMS_NUMBER || newStep.action === WorkflowActions.WHATSAPP_NUMBER;
await ctx.prisma.workflowStep.update({
@@ -613,12 +595,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
//added steps
const addedSteps = steps.map((s) => {
if (s.id <= 0) {
- if (isSMSOrWhatsappAction(s.action) && !isTextMessageToAttendeeAction(s.action) && !hasPaidPlan) {
+ if (isSMSOrWhatsappAction(s.action) && !hasPaidPlan) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not available on free plan" });
}
- if (!hasOrgsPlan && isTextMessageToAttendeeAction(s.action)) {
- throw new TRPCError({ code: "UNAUTHORIZED", message: "Enterprise plan require" });
- }
const { id: _stepId, ...stepToAdd } = s;
return stepToAdd;
}
diff --git a/packages/ui/components/form/select/components.tsx b/packages/ui/components/form/select/components.tsx
index e46ae36160..d41557ea9c 100644
--- a/packages/ui/components/form/select/components.tsx
+++ b/packages/ui/components/form/select/components.tsx
@@ -4,7 +4,7 @@ import { components as reactSelectComponents } from "react-select";
import { classNames } from "@calcom/lib";
import { Icon } from "../../..";
-import { UpgradeTeamsBadge, UpgradeOrgsBadge } from "../../badge";
+import { UpgradeTeamsBadge } from "../../badge";
import type { SelectProps } from "./Select";
export const InputComponent = <
@@ -31,7 +31,6 @@ type ExtendedOption = {
value: string | number;
label: string;
needsTeamsUpgrade?: boolean;
- needsOrgsUpgrade?: boolean;
};
export const OptionComponent = <
@@ -48,13 +47,7 @@ export const OptionComponent = <
{props.label || <> >}
- {(props.data as unknown as ExtendedOption).needsTeamsUpgrade ? (
-
- ) : (props.data as unknown as ExtendedOption).needsOrgsUpgrade ? (
-
- ) : (
- <>>
- )}
+ {(props.data as unknown as ExtendedOption).needsTeamsUpgrade ? : <>>}
{props.isSelected && }