Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b5ea649d4 | ||
|
|
0802ebb17b | ||
|
|
29b8d68f45 | ||
|
|
6994cac5e0 | ||
|
|
df9b72583d | ||
|
|
553eab59e6 | ||
|
|
8d675fd8b6 |
Vendored
+1
-1
@@ -177,7 +177,7 @@ export class OAuthClientUsersController {
|
||||
const { id } = await this.validateManagedUserOwnership(oAuthClientId, userId);
|
||||
|
||||
const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } =
|
||||
await this.tokensRepository.createOAuthTokens(oAuthClientId, id, true);
|
||||
await this.tokensRepository.forceRefreshOAuthTokens(oAuthClientId, id);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Injectable } from "@nestjs/common";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { PlatformAuthorizationToken } from "@prisma/client";
|
||||
import { DateTime } from "luxon";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
@Injectable()
|
||||
export class TokensRepository {
|
||||
@@ -50,31 +51,18 @@ export class TokensRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async createOAuthTokens(clientId: string, ownerId: number, deleteOld?: boolean) {
|
||||
if (deleteOld) {
|
||||
try {
|
||||
await this.dbWrite.prisma.$transaction([
|
||||
this.dbWrite.prisma.accessToken.deleteMany({
|
||||
where: { client: { id: clientId }, userId: ownerId },
|
||||
}),
|
||||
this.dbWrite.prisma.refreshToken.deleteMany({
|
||||
where: {
|
||||
client: { id: clientId },
|
||||
userId: ownerId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
} catch (err) {
|
||||
this.logger.error("createOAuthTokens - Failed to delete old tokens", err);
|
||||
}
|
||||
}
|
||||
|
||||
async createOAuthTokens(clientId: string, ownerId: number) {
|
||||
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
|
||||
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
||||
const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([
|
||||
this.dbWrite.prisma.accessToken.create({
|
||||
data: {
|
||||
secret: this.jwtService.signAccessToken({ clientId, ownerId, expiresAt: accessExpiry.valueOf() }),
|
||||
secret: this.jwtService.signAccessToken({
|
||||
clientId,
|
||||
ownerId,
|
||||
expiresAt: accessExpiry.valueOf(),
|
||||
jti: uuidv4(),
|
||||
}),
|
||||
expiresAt: accessExpiry,
|
||||
client: { connect: { id: clientId } },
|
||||
owner: { connect: { id: ownerId } },
|
||||
@@ -82,7 +70,12 @@ export class TokensRepository {
|
||||
}),
|
||||
this.dbWrite.prisma.refreshToken.create({
|
||||
data: {
|
||||
secret: this.jwtService.signRefreshToken({ clientId, ownerId, expiresAt: refreshExpiry.valueOf() }),
|
||||
secret: this.jwtService.signRefreshToken({
|
||||
clientId,
|
||||
ownerId,
|
||||
expiresAt: refreshExpiry.valueOf(),
|
||||
jti: uuidv4(),
|
||||
}),
|
||||
expiresAt: refreshExpiry,
|
||||
client: { connect: { id: clientId } },
|
||||
owner: { connect: { id: ownerId } },
|
||||
@@ -98,6 +91,57 @@ export class TokensRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async forceRefreshOAuthTokens(clientId: string, ownerId: number) {
|
||||
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
|
||||
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
||||
|
||||
const [_deletedAccessToken, _deletedRefreshToken, accessToken, refreshToken] =
|
||||
await this.dbWrite.prisma.$transaction([
|
||||
this.dbWrite.prisma.accessToken.deleteMany({
|
||||
where: { client: { id: clientId }, userId: ownerId },
|
||||
}),
|
||||
this.dbWrite.prisma.refreshToken.deleteMany({
|
||||
where: {
|
||||
client: { id: clientId },
|
||||
userId: ownerId,
|
||||
},
|
||||
}),
|
||||
this.dbWrite.prisma.accessToken.create({
|
||||
data: {
|
||||
secret: this.jwtService.signAccessToken({
|
||||
clientId,
|
||||
ownerId,
|
||||
expiresAt: accessExpiry.valueOf(),
|
||||
jti: uuidv4(),
|
||||
}),
|
||||
expiresAt: accessExpiry,
|
||||
client: { connect: { id: clientId } },
|
||||
owner: { connect: { id: ownerId } },
|
||||
},
|
||||
}),
|
||||
this.dbWrite.prisma.refreshToken.create({
|
||||
data: {
|
||||
secret: this.jwtService.signRefreshToken({
|
||||
clientId,
|
||||
ownerId,
|
||||
expiresAt: refreshExpiry.valueOf(),
|
||||
jti: uuidv4(),
|
||||
}),
|
||||
expiresAt: refreshExpiry,
|
||||
client: { connect: { id: clientId } },
|
||||
owner: { connect: { id: ownerId } },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
accessToken: accessToken.secret,
|
||||
accessTokenExpiresAt: accessToken.expiresAt,
|
||||
refreshToken: refreshToken.secret,
|
||||
refreshTokenExpiresAt: refreshToken.expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
async getAccessTokenExpiryDate(accessTokenSecret: string) {
|
||||
const accessToken = await this.dbRead.prisma.accessToken.findFirst({
|
||||
where: {
|
||||
@@ -140,6 +184,7 @@ export class TokensRepository {
|
||||
ownerId: tokenUserId,
|
||||
expiresAt: accessExpiry.valueOf(),
|
||||
userId: tokenUserId,
|
||||
jti: uuidv4(),
|
||||
}),
|
||||
expiresAt: accessExpiry,
|
||||
client: { connect: { id: clientId } },
|
||||
@@ -155,6 +200,7 @@ export class TokensRepository {
|
||||
ownerId: tokenUserId,
|
||||
expiresAt: refreshExpiry.valueOf(),
|
||||
userId: tokenUserId,
|
||||
jti: uuidv4(),
|
||||
}),
|
||||
expiresAt: refreshExpiry,
|
||||
client: { connect: { id: clientId } },
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { getTranslate } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
|
||||
import UpgradeTipWrapper from "./UpgradeTipWrapper";
|
||||
|
||||
export default async function InsightsLayout({ children }: { children: React.ReactNode }) {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
|
||||
|
||||
if (!insightsEnabled) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
|
||||
import InsightsPage from "~/insights/insights-view";
|
||||
|
||||
@@ -15,12 +12,5 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
export default async function Page() {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
|
||||
|
||||
if (!insightsEnabled) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <InsightsPage />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
|
||||
import InsightsVirtualQueuesPage from "~/insights/insights-virtual-queues-view";
|
||||
|
||||
@@ -15,12 +12,5 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
export default async function Page() {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
|
||||
|
||||
if (!insightsEnabled) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <InsightsVirtualQueuesPage />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
|
||||
import InsightsRoutingPage from "~/insights/insights-routing-view";
|
||||
|
||||
@@ -15,12 +12,5 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
export default async function Page() {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
|
||||
|
||||
if (!insightsEnabled) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <InsightsRoutingPage />;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { ProgressBar } from "@tremor/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||
import { IS_SMS_CREDITS_ENABLED } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
@@ -61,7 +63,19 @@ export default function BillingCredits() {
|
||||
<div className="border-subtle mt-8 space-y-6 rounded-lg border px-6 py-6 pb-6 text-sm sm:space-y-8">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold">{t("credits")}</h2>
|
||||
<p>{t("view_and_manage_credits_description")}</p>
|
||||
<ServerTrans
|
||||
t={t}
|
||||
i18nKey="view_and_manage_credits_description"
|
||||
components={[
|
||||
<Link
|
||||
key="Credit System"
|
||||
className="underline underline-offset-2"
|
||||
target="_blank"
|
||||
href="https://cal.com/help/billing-and-usage/messaging-credits">
|
||||
Learn more
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
<div className="-mx-6 mt-6">
|
||||
<hr className="border-subtle" />
|
||||
</div>
|
||||
@@ -90,7 +104,6 @@ export default function BillingCredits() {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<Label>
|
||||
{creditsData.credits.totalMonthlyCredits ? t("additional_credits") : t("available_credits")}
|
||||
</Label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.2.17",
|
||||
"version": "5.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -76,7 +76,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const creditService = new CreditService();
|
||||
|
||||
if (countryCode === "US" || countryCode === "CA") {
|
||||
// SMS to US and CA are free on a team plan
|
||||
// SMS to US and CA are free
|
||||
let teamIdToCharge = parsedTeamId;
|
||||
|
||||
if (!teamIdToCharge && parsedUserId) {
|
||||
@@ -100,7 +100,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
credits: 0,
|
||||
});
|
||||
|
||||
return res.status(200).send(`SMS to US and CA are free on a team plan. Credits set to 0`);
|
||||
return res.status(200).send(`SMS to US and CA are free. Credits set to 0`);
|
||||
}
|
||||
|
||||
let orgId;
|
||||
|
||||
@@ -3024,7 +3024,7 @@
|
||||
"buy_credits": "Buy Credits",
|
||||
"credits": "Credits",
|
||||
"view_and_manage_credits": "View and manage credits",
|
||||
"view_and_manage_credits_description": "View and manage credits for sending SMS messages. One credit is worth 1¢ (USD)",
|
||||
"view_and_manage_credits_description": "View and manage credits for sending SMS messages. One credit is worth 1¢ (USD). <0>Learn more</0>",
|
||||
"buy_additional_credits": "Buy additional credits ($0.01 per credit)",
|
||||
"overview": "Overview",
|
||||
"organization_slug_taken": "Organization slug is already taken",
|
||||
|
||||
@@ -52,7 +52,7 @@ export const CreditBalanceLimitReachedEmail = (
|
||||
<div style={{ textAlign: "center", marginTop: "24px" }}>
|
||||
<CallToAction
|
||||
label={user.t("buy_credits")}
|
||||
href={`${WEBAPP_URL}/settings/teams/${user.id}/credits`}
|
||||
href={`${WEBAPP_URL}/settings/billing`}
|
||||
endIconName="linkIcon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const CreditBalanceLowWarningEmail = (
|
||||
<div style={{ textAlign: "center", marginTop: "24px" }}>
|
||||
<CallToAction
|
||||
label={user.t("buy_credits")}
|
||||
href={`${WEBAPP_URL}/settings/teams/${user.id}/billing`}
|
||||
href={`${WEBAPP_URL}/settings/billing`}
|
||||
endIconName="linkIcon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AssignmentReasonEnum } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
import {
|
||||
@@ -1301,6 +1302,8 @@ async function handler(
|
||||
})
|
||||
);
|
||||
|
||||
let assignmentReason: { reasonEnum: AssignmentReasonEnum; reasonString: string } | undefined;
|
||||
|
||||
try {
|
||||
if (!isDryRun) {
|
||||
booking = await createBooking({
|
||||
@@ -1341,7 +1344,7 @@ async function handler(
|
||||
// If it's a round robin event, record the reason for the host assignment
|
||||
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
|
||||
if (reqBody.crmOwnerRecordType && reqBody.crmAppSlug && contactOwnerEmail && routingFormResponseId) {
|
||||
await AssignmentReasonRecorder.CRMOwnership({
|
||||
assignmentReason = await AssignmentReasonRecorder.CRMOwnership({
|
||||
bookingId: booking.id,
|
||||
crmAppSlug: reqBody.crmAppSlug,
|
||||
teamMemberEmail: contactOwnerEmail,
|
||||
@@ -1349,7 +1352,7 @@ async function handler(
|
||||
routingFormResponseId,
|
||||
});
|
||||
} else if (routingFormResponseId && teamId) {
|
||||
await AssignmentReasonRecorder.routingFormRoute({
|
||||
assignmentReason = await AssignmentReasonRecorder.routingFormRoute({
|
||||
bookingId: booking.id,
|
||||
routingFormResponseId,
|
||||
organizerId: organizerUser.id,
|
||||
@@ -1885,6 +1888,7 @@ async function handler(
|
||||
status: "ACCEPTED",
|
||||
smsReminderNumber: booking?.smsReminderNumber || undefined,
|
||||
rescheduledBy: reqBody.rescheduledBy,
|
||||
...(assignmentReason ? { assignmentReason: [assignmentReason] } : {}),
|
||||
};
|
||||
|
||||
if (bookingRequiresPayment) {
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler = async (data: SWHMap["checkout.session.completed"]["data"]) => {
|
||||
const userId = session.metadata?.userId ? Number(session.metadata.userId) : undefined;
|
||||
|
||||
if (!teamId && !userId) {
|
||||
throw new HttpCode(400, "Team id and user id, but at least one is required");
|
||||
throw new HttpCode(400, "Team id and user id are missing, but at least one is required");
|
||||
}
|
||||
|
||||
const lineItems = await stripe.checkout.sessions.listLineItems(session.id);
|
||||
|
||||
@@ -395,7 +395,6 @@ describe("CreditService", () => {
|
||||
vi.spyOn(CreditService.prototype, "getMonthlyCredits").mockResolvedValue(500);
|
||||
|
||||
const result = await creditService.getAllCreditsForTeam(1);
|
||||
console.log(`tesult ${JSON.stringify(result)}`);
|
||||
expect(result).toEqual({
|
||||
totalMonthlyCredits: 500,
|
||||
totalRemainingMonthlyCredits: 420, // 500 - (50 + 30)
|
||||
|
||||
@@ -113,13 +113,21 @@ export default class AssignmentReasonRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
const reasonEnum = AssignmentReasonEnum.ROUTING_FORM_ROUTING;
|
||||
const reasonString = attributeValues.join(", ");
|
||||
|
||||
await prisma.assignmentReason.create({
|
||||
data: {
|
||||
bookingId: bookingId,
|
||||
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING,
|
||||
reasonString: attributeValues.join(", "),
|
||||
reasonEnum,
|
||||
reasonString,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
reasonEnum,
|
||||
reasonString,
|
||||
};
|
||||
}
|
||||
|
||||
// Separate method to handle rerouting
|
||||
@@ -148,13 +156,20 @@ export default class AssignmentReasonRecorder {
|
||||
|
||||
if (!crmRoutingReason || !crmRoutingReason.assignmentReason) return;
|
||||
|
||||
const { reasonEnum, assignmentReason } = crmRoutingReason;
|
||||
|
||||
await prisma.assignmentReason.create({
|
||||
data: {
|
||||
bookingId,
|
||||
reasonEnum: crmRoutingReason.reasonEnum,
|
||||
reasonString: crmRoutingReason.assignmentReason,
|
||||
reasonEnum,
|
||||
reasonString: assignmentReason,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
reasonEnum,
|
||||
reasonString: assignmentReason,
|
||||
};
|
||||
}
|
||||
static roundRobinReassignment = withReporting(
|
||||
AssignmentReasonRecorder._roundRobinReassignment,
|
||||
@@ -180,6 +195,11 @@ export default class AssignmentReasonRecorder {
|
||||
},
|
||||
});
|
||||
|
||||
const reasonEnum =
|
||||
reassignmentType === RRReassignmentType.MANUAL
|
||||
? AssignmentReasonEnum.REASSIGNED
|
||||
: AssignmentReasonEnum.RR_REASSIGNED;
|
||||
|
||||
const reasonString = `Reassigned by: ${reassignedBy?.username || "team member"}. ${
|
||||
reassignReason ? `Reason: ${reassignReason}` : ""
|
||||
}`;
|
||||
@@ -187,12 +207,14 @@ export default class AssignmentReasonRecorder {
|
||||
await prisma.assignmentReason.create({
|
||||
data: {
|
||||
bookingId: bookingId,
|
||||
reasonEnum:
|
||||
reassignmentType === RRReassignmentType.MANUAL
|
||||
? AssignmentReasonEnum.REASSIGNED
|
||||
: AssignmentReasonEnum.RR_REASSIGNED,
|
||||
reasonEnum,
|
||||
reasonString,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
reasonEnum,
|
||||
reasonString,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export class CreditsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
static async findCreditBalanceWithExpenseLogs({ teamId }: { teamId?: number }) {
|
||||
static async findCreditBalanceWithExpenseLogs({ teamId }: { teamId: number }) {
|
||||
return await prisma.creditBalance.findUnique({
|
||||
where: {
|
||||
teamId,
|
||||
|
||||
@@ -34,9 +34,9 @@ export const buyCreditsHandler = async ({ ctx, input }: BuyCreditsOptions) => {
|
||||
}
|
||||
} else {
|
||||
// if user id is part of a team, user can't buy credits for themselves
|
||||
const membership = await MembershipRepository.findAllAcceptedMemberships(ctx.user.id);
|
||||
const memberships = await MembershipRepository.findAllAcceptedMemberships(ctx.user.id);
|
||||
|
||||
if (membership.length > 0) {
|
||||
if (memberships.length > 0) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
|
||||
@@ -25,9 +25,9 @@ export const getAllCreditsHandler = async ({ ctx, input }: GetAllCreditsOptions)
|
||||
}
|
||||
} else {
|
||||
//if user is part of team, don't return any credits if teamId is not given
|
||||
const membership = await MembershipRepository.findAllAcceptedMemberships(ctx.user.id);
|
||||
const memberships = await MembershipRepository.findAllAcceptedMemberships(ctx.user.id);
|
||||
|
||||
if (membership.length > 0) {
|
||||
if (memberships.length > 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,28 @@ export const verifyEmailSender = async (email: string, userId: number, teamId: n
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a verified secondary email of the user
|
||||
const secondaryEmail = await prisma.secondaryEmail.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
email,
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (secondaryEmail) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
@@ -149,6 +171,12 @@ export const verifyEmailSender = async (email: string, userId: number, teamId: n
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
secondaryEmails: {
|
||||
select: {
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -166,9 +194,18 @@ export const verifyEmailSender = async (email: string, userId: number, teamId: n
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "You are not a member of this team" });
|
||||
}
|
||||
|
||||
const teamMemberEmail = team.members.filter((member) => member.user.email === email);
|
||||
let foundTeamMember = team.members.find((member) => member.user.email === email);
|
||||
|
||||
if (teamMemberEmail) {
|
||||
// Only check secondary emails if no match was found with primary email
|
||||
if (!foundTeamMember) {
|
||||
foundTeamMember = team.members.find((member) =>
|
||||
member.user.secondaryEmails.some(
|
||||
(secondary) => secondary.email === email && !!secondary.emailVerified
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (foundTeamMember) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
|
||||
Reference in New Issue
Block a user