attach phone numbers to twilio account
This commit is contained in:
parent
c47b57e4bf
commit
3ddd0d73ea
@ -5,6 +5,7 @@ import { type Message, Prisma } from "@prisma/client";
|
|||||||
|
|
||||||
import db from "~/utils/db.server";
|
import db from "~/utils/db.server";
|
||||||
import { requireLoggedIn } from "~/utils/auth.server";
|
import { requireLoggedIn } from "~/utils/auth.server";
|
||||||
|
import { redirect } from "@remix-run/node";
|
||||||
|
|
||||||
type ConversationType = {
|
type ConversationType = {
|
||||||
recipient: string;
|
recipient: string;
|
||||||
@ -17,16 +18,20 @@ export type ConversationLoaderData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loader: LoaderFunction = async ({ request, params }) => {
|
const loader: LoaderFunction = async ({ request, params }) => {
|
||||||
const { organization } = await requireLoggedIn(request);
|
const { twilio } = await requireLoggedIn(request);
|
||||||
|
if (!twilio) {
|
||||||
|
return redirect("/messages");
|
||||||
|
}
|
||||||
|
|
||||||
|
const twilioAccountSid = twilio.accountSid;
|
||||||
const recipient = decodeURIComponent(params.recipient ?? "");
|
const recipient = decodeURIComponent(params.recipient ?? "");
|
||||||
const conversation = await getConversation(recipient);
|
const conversation = await getConversation(recipient);
|
||||||
|
|
||||||
return json<ConversationLoaderData>({ conversation });
|
return json<ConversationLoaderData>({ conversation });
|
||||||
|
|
||||||
async function getConversation(recipient: string): Promise<ConversationType> {
|
async function getConversation(recipient: string): Promise<ConversationType> {
|
||||||
const organizationId = organization.id;
|
|
||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({
|
||||||
where: { organizationId_isCurrent: { organizationId, isCurrent: true } },
|
where: { twilioAccountSid_isCurrent: { twilioAccountSid, isCurrent: true } },
|
||||||
});
|
});
|
||||||
if (!phoneNumber || phoneNumber.isFetchingMessages) {
|
if (!phoneNumber || phoneNumber.isFetchingMessages) {
|
||||||
throw new Error("unreachable");
|
throw new Error("unreachable");
|
||||||
|
@ -41,7 +41,15 @@ const action: ActionFunction = async ({ request }) => {
|
|||||||
export type SetPhoneNumberActionData = FormActionData<typeof validations, "setPhoneNumber">;
|
export type SetPhoneNumberActionData = FormActionData<typeof validations, "setPhoneNumber">;
|
||||||
|
|
||||||
async function setPhoneNumber(request: Request, formData: unknown) {
|
async function setPhoneNumber(request: Request, formData: unknown) {
|
||||||
const { organization } = await requireLoggedIn(request);
|
const { organization, twilio } = await requireLoggedIn(request);
|
||||||
|
if (!twilio) {
|
||||||
|
return badRequest<SetPhoneNumberActionData>({
|
||||||
|
setPhoneNumber: {
|
||||||
|
errors: { general: "Connect your Twilio account first" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const validation = validate(validations.setPhoneNumber, formData);
|
const validation = validate(validations.setPhoneNumber, formData);
|
||||||
if (validation.errors) {
|
if (validation.errors) {
|
||||||
return badRequest<SetPhoneNumberActionData>({ setPhoneNumber: { errors: validation.errors } });
|
return badRequest<SetPhoneNumberActionData>({ setPhoneNumber: { errors: validation.errors } });
|
||||||
@ -49,7 +57,7 @@ async function setPhoneNumber(request: Request, formData: unknown) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await db.phoneNumber.update({
|
await db.phoneNumber.update({
|
||||||
where: { organizationId_isCurrent: { organizationId: organization.id, isCurrent: true } },
|
where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilio.accountSid, isCurrent: true } },
|
||||||
data: { isCurrent: false },
|
data: { isCurrent: false },
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -150,7 +158,7 @@ async function setTwilioCredentials(request: Request, formData: unknown) {
|
|||||||
await db.phoneNumber.create({
|
await db.phoneNumber.create({
|
||||||
data: {
|
data: {
|
||||||
id: phoneNumberId,
|
id: phoneNumberId,
|
||||||
organizationId: organization.id,
|
twilioAccountSid,
|
||||||
number: phoneNumber.phoneNumber,
|
number: phoneNumber.phoneNumber,
|
||||||
isCurrent: false,
|
isCurrent: false,
|
||||||
isFetchingCalls: true,
|
isFetchingCalls: true,
|
||||||
@ -187,7 +195,7 @@ async function setTwilioCredentials(request: Request, formData: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshPhoneNumbers(request: Request) {
|
async function refreshPhoneNumbers(request: Request) {
|
||||||
const { organization, twilio } = await requireLoggedIn(request);
|
const { twilio } = await requireLoggedIn(request);
|
||||||
if (!twilio) {
|
if (!twilio) {
|
||||||
throw new Error("unreachable");
|
throw new Error("unreachable");
|
||||||
}
|
}
|
||||||
@ -205,13 +213,19 @@ async function refreshPhoneNumbers(request: Request) {
|
|||||||
await db.phoneNumber.create({
|
await db.phoneNumber.create({
|
||||||
data: {
|
data: {
|
||||||
id: phoneNumberId,
|
id: phoneNumberId,
|
||||||
organizationId: organization.id,
|
twilioAccountSid: twilioAccount.accountSid,
|
||||||
number: phoneNumber.phoneNumber,
|
number: phoneNumber.phoneNumber,
|
||||||
isCurrent: false,
|
isCurrent: false,
|
||||||
isFetchingCalls: true,
|
isFetchingCalls: true,
|
||||||
isFetchingMessages: true,
|
isFetchingMessages: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code !== "P2002") {
|
||||||
|
// if it's not a duplicate, it's a real error we need to handle
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetchPhoneCallsQueue.add(`fetch calls of id=${phoneNumberId}`, {
|
fetchPhoneCallsQueue.add(`fetch calls of id=${phoneNumberId}`, {
|
||||||
@ -221,12 +235,6 @@ async function refreshPhoneNumbers(request: Request) {
|
|||||||
phoneNumberId,
|
phoneNumberId,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code !== "P2002") {
|
|
||||||
// if it's not a duplicate, it's a real error we need to handle
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -10,13 +10,16 @@ import type { SetPhoneNumberActionData } from "~/features/settings/actions/phone
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export default function PhoneNumberForm() {
|
export default function PhoneNumberForm() {
|
||||||
const { twilio } = useSession();
|
const { twilio, phoneNumber } = useSession();
|
||||||
const fetcher = useFetcher();
|
const fetcher = useFetcher();
|
||||||
const transition = useTransition();
|
const transition = useTransition();
|
||||||
const actionData = useActionData<SetPhoneNumberActionData>()?.setPhoneNumber;
|
const actionData = useActionData<SetPhoneNumberActionData>()?.setPhoneNumber;
|
||||||
const availablePhoneNumbers = useLoaderData<PhoneSettingsLoaderData>().phoneNumbers;
|
const availablePhoneNumbers = useLoaderData<PhoneSettingsLoaderData>().phoneNumbers;
|
||||||
|
|
||||||
const isSubmitting = transition.state === "submitting";
|
const actionSubmitted = transition.submission?.formData.get("_action");
|
||||||
|
const isCurrentFormTransition =
|
||||||
|
!!actionSubmitted && ["setPhoneNumber", "refreshPhoneNumbers"].includes(actionSubmitted.toString());
|
||||||
|
const isSubmitting = isCurrentFormTransition && transition.state === "submitting";
|
||||||
const isSuccess = actionData?.submitted === true;
|
const isSuccess = actionData?.submitted === true;
|
||||||
const errors = actionData?.errors;
|
const errors = actionData?.errors;
|
||||||
const topErrorMessage = errors?.general ?? errors?.phoneNumberSid;
|
const topErrorMessage = errors?.general ?? errors?.phoneNumberSid;
|
||||||
|
@ -20,7 +20,7 @@ export default function TwilioConnect() {
|
|||||||
|
|
||||||
const topErrorMessage = actionData?.errors?.general;
|
const topErrorMessage = actionData?.errors?.general;
|
||||||
const isError = typeof topErrorMessage !== "undefined";
|
const isError = typeof topErrorMessage !== "undefined";
|
||||||
const isCurrentFormTransition = transition.submission?.formData.get("_action") === "changePassword";
|
const isCurrentFormTransition = transition.submission?.formData.get("_action") === "setTwilioCredentials";
|
||||||
const isSubmitting = isCurrentFormTransition && transition.state === "submitting";
|
const isSubmitting = isCurrentFormTransition && transition.state === "submitting";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -20,7 +20,7 @@ const loader: LoaderFunction = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const phoneNumbers = await db.phoneNumber.findMany({
|
const phoneNumbers = await db.phoneNumber.findMany({
|
||||||
where: { organizationId: organization.id },
|
where: { twilioAccount: { organizationId: organization.id } },
|
||||||
select: { id: true, number: true, isCurrent: true },
|
select: { id: true, number: true, isCurrent: true },
|
||||||
orderBy: { id: Prisma.SortOrder.desc },
|
orderBy: { id: Prisma.SortOrder.desc },
|
||||||
});
|
});
|
||||||
|
@ -12,24 +12,14 @@ export default Queue<Payload>("fetch messages", async ({ data }) => {
|
|||||||
const { phoneNumberId } = data;
|
const { phoneNumberId } = data;
|
||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({
|
||||||
where: { id: phoneNumberId },
|
where: { id: phoneNumberId },
|
||||||
include: {
|
include: { twilioAccount: true },
|
||||||
organization: {
|
|
||||||
select: { twilioAccount: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (!phoneNumber) {
|
if (!phoneNumber) {
|
||||||
logger.warn(`No phone number found with id=${phoneNumberId}`);
|
logger.warn(`No phone number found with id=${phoneNumberId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const twilioAccount = phoneNumber.organization.twilioAccount;
|
const twilioClient = getTwilioClient(phoneNumber.twilioAccount);
|
||||||
if (!twilioAccount) {
|
|
||||||
logger.warn(`Phone number with id=${phoneNumberId} doesn't have a connected twilio account`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const twilioClient = getTwilioClient(twilioAccount);
|
|
||||||
const [sent, received] = await Promise.all([
|
const [sent, received] = await Promise.all([
|
||||||
twilioClient.messages.list({ from: phoneNumber.number }),
|
twilioClient.messages.list({ from: phoneNumber.number }),
|
||||||
twilioClient.messages.list({ to: phoneNumber.number }),
|
twilioClient.messages.list({ to: phoneNumber.number }),
|
||||||
|
@ -12,24 +12,14 @@ export default Queue<Payload>("fetch phone calls", async ({ data }) => {
|
|||||||
const { phoneNumberId } = data;
|
const { phoneNumberId } = data;
|
||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({
|
||||||
where: { id: phoneNumberId },
|
where: { id: phoneNumberId },
|
||||||
include: {
|
include: { twilioAccount: true },
|
||||||
organization: {
|
|
||||||
select: { twilioAccount: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (!phoneNumber) {
|
if (!phoneNumber) {
|
||||||
logger.warn(`No phone number found with id=${phoneNumberId}`);
|
logger.warn(`No phone number found with id=${phoneNumberId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const twilioAccount = phoneNumber.organization.twilioAccount;
|
const twilioClient = getTwilioClient(phoneNumber.twilioAccount);
|
||||||
if (!twilioAccount) {
|
|
||||||
logger.warn(`Phone number with id=${phoneNumberId} doesn't have a connected twilio account`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const twilioClient = getTwilioClient(twilioAccount);
|
|
||||||
const [callsSent, callsReceived] = await Promise.all([
|
const [callsSent, callsReceived] = await Promise.all([
|
||||||
twilioClient.calls.list({ from: phoneNumber.number }),
|
twilioClient.calls.list({ from: phoneNumber.number }),
|
||||||
twilioClient.calls.list({ to: phoneNumber.number }),
|
twilioClient.calls.list({ to: phoneNumber.number }),
|
||||||
|
@ -16,24 +16,14 @@ export default Queue<Payload>("insert incoming message", async ({ data }) => {
|
|||||||
logger.info(`received message ${messageSid} for ${phoneNumberId}`);
|
logger.info(`received message ${messageSid} for ${phoneNumberId}`);
|
||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({
|
||||||
where: { id: phoneNumberId },
|
where: { id: phoneNumberId },
|
||||||
include: {
|
include: { twilioAccount: true },
|
||||||
organization: {
|
|
||||||
select: { twilioAccount: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (!phoneNumber) {
|
if (!phoneNumber) {
|
||||||
logger.warn(`No phone number found with id=${phoneNumberId}`);
|
logger.warn(`No phone number found with id=${phoneNumberId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const twilioAccount = phoneNumber.organization.twilioAccount;
|
const twilioClient = getTwilioClient(phoneNumber.twilioAccount);
|
||||||
if (!twilioAccount) {
|
|
||||||
logger.warn(`Phone number with id=${phoneNumberId} doesn't have a connected twilio account`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const twilioClient = getTwilioClient(twilioAccount);
|
|
||||||
const message = await twilioClient.messages.get(messageSid).fetch();
|
const message = await twilioClient.messages.get(messageSid).fetch();
|
||||||
const status = translateMessageStatus(message.status);
|
const status = translateMessageStatus(message.status);
|
||||||
const direction = translateMessageDirection(message.direction);
|
const direction = translateMessageDirection(message.direction);
|
||||||
|
@ -13,10 +13,7 @@ type Payload = {
|
|||||||
|
|
||||||
export default Queue<Payload>("insert messages", async ({ data }) => {
|
export default Queue<Payload>("insert messages", async ({ data }) => {
|
||||||
const { messages, phoneNumberId } = data;
|
const { messages, phoneNumberId } = data;
|
||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({ where: { id: phoneNumberId } });
|
||||||
where: { id: phoneNumberId },
|
|
||||||
include: { organization: true },
|
|
||||||
});
|
|
||||||
if (!phoneNumber) {
|
if (!phoneNumber) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,7 @@ type Payload = {
|
|||||||
|
|
||||||
export default Queue<Payload>("insert phone calls", async ({ data }) => {
|
export default Queue<Payload>("insert phone calls", async ({ data }) => {
|
||||||
const { calls, phoneNumberId } = data;
|
const { calls, phoneNumberId } = data;
|
||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({ where: { id: phoneNumberId } });
|
||||||
where: { id: phoneNumberId },
|
|
||||||
include: { organization: true },
|
|
||||||
});
|
|
||||||
if (!phoneNumber) {
|
if (!phoneNumber) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -39,8 +36,8 @@ export default Queue<Payload>("insert phone calls", async ({ data }) => {
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||||
|
|
||||||
const ddd = await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true });
|
const { count } = await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true });
|
||||||
logger.info(`inserted ${ddd.count || "no"} new phone calls for phoneNumberId=${phoneNumberId}`);
|
logger.info(`inserted ${count} new phone calls for phoneNumberId=${phoneNumberId}`);
|
||||||
|
|
||||||
if (!phoneNumber.isFetchingCalls) {
|
if (!phoneNumber.isFetchingCalls) {
|
||||||
return;
|
return;
|
||||||
|
@ -14,22 +14,18 @@ type Payload = {
|
|||||||
export default Queue<Payload>("set twilio webhooks", async ({ data }) => {
|
export default Queue<Payload>("set twilio webhooks", async ({ data }) => {
|
||||||
const { phoneNumberId, organizationId } = data;
|
const { phoneNumberId, organizationId } = data;
|
||||||
const phoneNumber = await db.phoneNumber.findFirst({
|
const phoneNumber = await db.phoneNumber.findFirst({
|
||||||
where: { id: phoneNumberId, organizationId },
|
where: { id: phoneNumberId, twilioAccount: { organizationId } },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
|
||||||
select: {
|
|
||||||
twilioAccount: {
|
twilioAccount: {
|
||||||
select: { accountSid: true, twimlAppSid: true, authToken: true },
|
select: { accountSid: true, twimlAppSid: true, authToken: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (!phoneNumber || !phoneNumber.organization.twilioAccount) {
|
if (!phoneNumber) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const twilioAccount = phoneNumber.organization.twilioAccount;
|
const twilioAccount = phoneNumber.twilioAccount;
|
||||||
const authToken = decrypt(twilioAccount.authToken);
|
const authToken = decrypt(twilioAccount.authToken);
|
||||||
const twilioClient = twilio(twilioAccount.accountSid, authToken);
|
const twilioClient = twilio(twilioAccount.accountSid, authToken);
|
||||||
const twimlApp = await getTwimlApplication(twilioClient, twilioAccount.twimlAppSid);
|
const twimlApp = await getTwimlApplication(twilioClient, twilioAccount.twimlAppSid);
|
||||||
|
@ -13,6 +13,8 @@ export const action: ActionFunction = async () => {
|
|||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({
|
||||||
where: { id: "PN4f11f0c4155dfb5d5ac8bbab2cc23cbc" },
|
where: { id: "PN4f11f0c4155dfb5d5ac8bbab2cc23cbc" },
|
||||||
select: {
|
select: {
|
||||||
|
twilioAccount: {
|
||||||
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
select: {
|
select: {
|
||||||
memberships: {
|
memberships: {
|
||||||
@ -21,8 +23,10 @@ export const action: ActionFunction = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const subscriptions = phoneNumber!.organization.memberships.flatMap(
|
const subscriptions = phoneNumber!.twilioAccount.organization.memberships.flatMap(
|
||||||
(membership) => membership.notificationSubscription,
|
(membership) => membership.notificationSubscription,
|
||||||
);
|
);
|
||||||
await notify(subscriptions, {
|
await notify(subscriptions, {
|
||||||
|
@ -22,11 +22,19 @@ export const action: ActionFunction = async ({ request }) => {
|
|||||||
const organizationId = body.From.slice("client:".length).split("__")[0];
|
const organizationId = body.From.slice("client:".length).split("__")[0];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const twilioAccount = await db.twilioAccount.findUnique({ where: { organizationId } });
|
||||||
|
if (!twilioAccount) {
|
||||||
|
// this shouldn't be happening
|
||||||
|
return new Response(null, { status: 402 });
|
||||||
|
}
|
||||||
|
|
||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({
|
||||||
where: { organizationId_isCurrent: { organizationId, isCurrent: true } },
|
where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilioAccount.accountSid, isCurrent: true } },
|
||||||
|
include: {
|
||||||
|
twilioAccount: {
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
select: {
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
@ -39,19 +47,20 @@ export const action: ActionFunction = async ({ request }) => {
|
|||||||
},
|
},
|
||||||
orderBy: { lastEventTime: Prisma.SortOrder.desc },
|
orderBy: { lastEventTime: Prisma.SortOrder.desc },
|
||||||
},
|
},
|
||||||
twilioAccount: true,
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (phoneNumber?.organization.subscriptions.length === 0) {
|
if (phoneNumber?.twilioAccount.organization.subscriptions.length === 0) {
|
||||||
// decline the outgoing call because
|
// decline the outgoing call because
|
||||||
// the organization is on the free plan
|
// the organization is on the free plan
|
||||||
return new Response(null, { status: 402 });
|
return new Response(null, { status: 402 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedAuthToken = phoneNumber?.organization.twilioAccount?.authToken;
|
const encryptedAuthToken = phoneNumber?.twilioAccount.authToken;
|
||||||
const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
|
const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
|
||||||
if (
|
if (
|
||||||
!phoneNumber ||
|
!phoneNumber ||
|
||||||
|
@ -20,8 +20,10 @@ export const action: ActionFunction = async ({ request }) => {
|
|||||||
const phoneNumbers = await db.phoneNumber.findMany({
|
const phoneNumbers = await db.phoneNumber.findMany({
|
||||||
where: { number: body.To },
|
where: { number: body.To },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
twilioAccount: {
|
||||||
include: {
|
include: {
|
||||||
|
organization: {
|
||||||
|
select: {
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
@ -34,7 +36,8 @@ export const action: ActionFunction = async ({ request }) => {
|
|||||||
},
|
},
|
||||||
orderBy: { lastEventTime: Prisma.SortOrder.desc },
|
orderBy: { lastEventTime: Prisma.SortOrder.desc },
|
||||||
},
|
},
|
||||||
twilioAccount: true,
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -45,7 +48,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const phoneNumbersWithActiveSub = phoneNumbers.filter(
|
const phoneNumbersWithActiveSub = phoneNumbers.filter(
|
||||||
(phoneNumber) => phoneNumber.organization.subscriptions.length > 0,
|
(phoneNumber) => phoneNumber.twilioAccount.organization.subscriptions.length > 0,
|
||||||
);
|
);
|
||||||
if (phoneNumbersWithActiveSub.length === 0) {
|
if (phoneNumbersWithActiveSub.length === 0) {
|
||||||
// accept the webhook but don't store incoming message
|
// accept the webhook but don't store incoming message
|
||||||
@ -57,7 +60,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||||||
// if multiple organizations have the same number
|
// if multiple organizations have the same number
|
||||||
// find the organization currently using that phone number
|
// find the organization currently using that phone number
|
||||||
// maybe we shouldn't let that happen by restricting a phone number to one org?
|
// maybe we shouldn't let that happen by restricting a phone number to one org?
|
||||||
const encryptedAuthToken = phoneNumber.organization.twilioAccount?.authToken;
|
const encryptedAuthToken = phoneNumber.twilioAccount.authToken;
|
||||||
const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
|
const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
|
||||||
return twilio.validateRequest(authToken, twilioSignature, smsUrl, body);
|
return twilio.validateRequest(authToken, twilioSignature, smsUrl, body);
|
||||||
});
|
});
|
||||||
|
@ -199,7 +199,7 @@ async function buildSessionData(id: string): Promise<SessionData> {
|
|||||||
}));
|
}));
|
||||||
const { twilioAccount, ...organization } = organizations[0];
|
const { twilioAccount, ...organization } = organizations[0];
|
||||||
const phoneNumber = await db.phoneNumber.findUnique({
|
const phoneNumber = await db.phoneNumber.findUnique({
|
||||||
where: { organizationId_isCurrent: { organizationId: organization.id, isCurrent: true } },
|
where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilioAccount?.accountSid ?? "", isCurrent: true } },
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
user: rest,
|
user: rest,
|
||||||
|
@ -151,7 +151,7 @@ CREATE TABLE "PhoneNumber" (
|
|||||||
"isCurrent" BOOLEAN NOT NULL,
|
"isCurrent" BOOLEAN NOT NULL,
|
||||||
"isFetchingMessages" BOOLEAN,
|
"isFetchingMessages" BOOLEAN,
|
||||||
"isFetchingCalls" BOOLEAN,
|
"isFetchingCalls" BOOLEAN,
|
||||||
"organizationId" TEXT NOT NULL,
|
"twilioAccountSid" TEXT NOT NULL,
|
||||||
|
|
||||||
CONSTRAINT "PhoneNumber_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "PhoneNumber_pkey" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
@ -195,7 +195,7 @@ CREATE INDEX "Message_phoneNumberId_recipient_idx" ON "Message"("phoneNumberId",
|
|||||||
CREATE INDEX "PhoneCall_phoneNumberId_recipient_idx" ON "PhoneCall"("phoneNumberId", "recipient");
|
CREATE INDEX "PhoneCall_phoneNumberId_recipient_idx" ON "PhoneCall"("phoneNumberId", "recipient");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "PhoneNumber_organizationId_isCurrent_key" ON "PhoneNumber"("organizationId", "isCurrent") WHERE ("isCurrent" = true);
|
CREATE UNIQUE INDEX "PhoneNumber_twilioAccountSid_isCurrent_key" ON "PhoneNumber"("twilioAccountSid", "isCurrent") WHERE ("isCurrent" = true);
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "NotificationSubscription_endpoint_key" ON "NotificationSubscription"("endpoint");
|
CREATE UNIQUE INDEX "NotificationSubscription_endpoint_key" ON "NotificationSubscription"("endpoint");
|
||||||
@ -228,7 +228,7 @@ ALTER TABLE "Message" ADD CONSTRAINT "Message_phoneNumberId_fkey" FOREIGN KEY ("
|
|||||||
ALTER TABLE "PhoneCall" ADD CONSTRAINT "PhoneCall_phoneNumberId_fkey" FOREIGN KEY ("phoneNumberId") REFERENCES "PhoneNumber"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "PhoneCall" ADD CONSTRAINT "PhoneCall_phoneNumberId_fkey" FOREIGN KEY ("phoneNumberId") REFERENCES "PhoneNumber"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "PhoneNumber" ADD CONSTRAINT "PhoneNumber_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "PhoneNumber" ADD CONSTRAINT "PhoneNumber_twilioAccountSid_fkey" FOREIGN KEY ("twilioAccountSid") REFERENCES "TwilioAccount"("accountSid") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "NotificationSubscription" ADD CONSTRAINT "NotificationSubscription_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "Membership"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "NotificationSubscription" ADD CONSTRAINT "NotificationSubscription_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "Membership"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
@ -15,9 +15,9 @@ model TwilioAccount {
|
|||||||
twimlAppSid String?
|
twimlAppSid String?
|
||||||
apiKeySid String?
|
apiKeySid String?
|
||||||
apiKeySecret String?
|
apiKeySecret String?
|
||||||
|
|
||||||
organizationId String @unique
|
organizationId String @unique
|
||||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
phoneNumbers PhoneNumber[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Organization {
|
model Organization {
|
||||||
@ -27,7 +27,6 @@ model Organization {
|
|||||||
|
|
||||||
twilioAccount TwilioAccount?
|
twilioAccount TwilioAccount?
|
||||||
memberships Membership[]
|
memberships Membership[]
|
||||||
phoneNumbers PhoneNumber[]
|
|
||||||
subscriptions Subscription[] // many subscriptions to keep a history
|
subscriptions Subscription[] // many subscriptions to keep a history
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,12 +138,12 @@ model PhoneNumber {
|
|||||||
isCurrent Boolean
|
isCurrent Boolean
|
||||||
isFetchingMessages Boolean?
|
isFetchingMessages Boolean?
|
||||||
isFetchingCalls Boolean?
|
isFetchingCalls Boolean?
|
||||||
organizationId String
|
twilioAccountSid String
|
||||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
twilioAccount TwilioAccount @relation(fields: [twilioAccountSid], references: [accountSid], onDelete: Cascade)
|
||||||
messages Message[]
|
messages Message[]
|
||||||
phoneCalls PhoneCall[]
|
phoneCalls PhoneCall[]
|
||||||
|
|
||||||
@@unique([organizationId, isCurrent])
|
@@unique([twilioAccountSid, isCurrent])
|
||||||
}
|
}
|
||||||
|
|
||||||
model NotificationSubscription {
|
model NotificationSubscription {
|
||||||
|
Loading…
Reference in New Issue
Block a user