feat: plain-thread-tier (#19463)
* tier select * thread tier * remove console logs * requested changes to getting user tier * test * fix test error * fix type errors on test * pull latest * route middleware fix * double tierIdentifier * updated test file * build error * plain cards fix up * dayjs * change orgs check to teams isOrgs check * removed uneeded prisma calls and changed teams check
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { cardExamples } from "@pages/api/plain/example-cards";
|
||||
import { apiRouteMiddleware } from "app/api/apiRouteMiddleware";
|
||||
import { createHmac } from "crypto";
|
||||
import { headers } from "next/headers";
|
||||
@@ -6,9 +5,439 @@ import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { CardComponent } from "@lib/plain/card-components";
|
||||
|
||||
const customerCardDisplay = (
|
||||
name: string,
|
||||
email: string,
|
||||
id: string,
|
||||
username: string,
|
||||
timeZone: string,
|
||||
emailVerified: Date | null,
|
||||
twoFactorEnabled: boolean | null,
|
||||
identityProvider: string | null,
|
||||
lastActiveAt: Date | null,
|
||||
teamName: string | null,
|
||||
teamSlug: string | null,
|
||||
isOrganization: boolean | null,
|
||||
stripeCustomerId: string | null
|
||||
): Card => {
|
||||
return {
|
||||
key: "customer-card",
|
||||
timeToLiveSeconds: null,
|
||||
components: [
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
text: "Basic Information",
|
||||
textColor: "NORMAL",
|
||||
textSize: "L",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Name",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: name || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: email || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Username",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: username || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "User ID",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: id || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: "M",
|
||||
},
|
||||
},
|
||||
// Account Settings Section Title
|
||||
{
|
||||
componentText: {
|
||||
text: "Account Settings",
|
||||
textColor: "NORMAL",
|
||||
textSize: "L",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Time Zone",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: timeZone || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Identity Provider",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: identityProvider || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Last Active At",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: lastActiveAt ? dayjs(lastActiveAt).fromNow() : "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: "M",
|
||||
},
|
||||
},
|
||||
// Team Information Section Title
|
||||
{
|
||||
componentText: {
|
||||
text: "Team Information",
|
||||
textColor: "NORMAL",
|
||||
textSize: "L",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Team Name",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: teamName || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Team Slug",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: teamSlug || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Is Organization",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: isOrganization ? "Yes" : "No",
|
||||
badgeColor: isOrganization ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
text: "Security & Billing",
|
||||
textColor: "NORMAL",
|
||||
textSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Stripe Customer ID",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: stripeCustomerId || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email Verified?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No",
|
||||
badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Two Factor Enabled?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: twoFactorEnabled === null ? "No" : twoFactorEnabled ? "Yes" : "No",
|
||||
badgeColor: twoFactorEnabled === null ? "RED" : twoFactorEnabled ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const Card = z.object({
|
||||
key: z.string(),
|
||||
timeToLiveSeconds: z.number().int().min(0).nullish().default(null),
|
||||
components: z.array(CardComponent).nullable(),
|
||||
});
|
||||
|
||||
type Card = z.infer<typeof Card>;
|
||||
|
||||
const cardExamples: ((
|
||||
name: string,
|
||||
email: string,
|
||||
id: string,
|
||||
username: string,
|
||||
timeZone: string,
|
||||
emailVerified: Date | null,
|
||||
twoFactorEnabled: boolean | null,
|
||||
identityProvider: string | null,
|
||||
lastActiveAt: Date | null,
|
||||
teamName: string | null,
|
||||
teamSlug: string | null,
|
||||
isOrganization: boolean | null,
|
||||
stripeCustomerId: string | null
|
||||
) => Card)[] = [customerCardDisplay];
|
||||
|
||||
const inputSchema = z.object({
|
||||
customer: z.object({
|
||||
name: z.string().optional(),
|
||||
@@ -37,7 +466,6 @@ async function handler(request: NextRequest) {
|
||||
const expectedSignature = createHmac("sha-256", process.env.PLAIN_HMAC_SECRET_KEY!)
|
||||
.update(JSON.stringify(requestBody))
|
||||
.digest("hex");
|
||||
|
||||
if (incomingSignature !== expectedSignature) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -15,6 +16,7 @@ const responseSchema = z.object({
|
||||
appId: z.string(),
|
||||
fullName: z.string(),
|
||||
chatAvatarUrl: z.string(),
|
||||
userTier: z.enum(["free", "teams", "enterprise"]),
|
||||
});
|
||||
|
||||
async function handler() {
|
||||
@@ -28,6 +30,34 @@ async function handler() {
|
||||
return NextResponse.json({ error: "Missing Plain Chat secret" }, { status: 500 });
|
||||
}
|
||||
|
||||
// Get user's team membership info
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: {
|
||||
teams: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check if user is part of a team and determine tier
|
||||
let userTier = "free";
|
||||
|
||||
if (user?.teams.length) {
|
||||
const teamMemberships = user.teams;
|
||||
|
||||
const isEnterprise = teamMemberships.some((membership) => membership.team.isOrganization === true);
|
||||
const isTeams = user?.teams.length > 0;
|
||||
|
||||
userTier = isEnterprise ? "enterprise" : isTeams ? "teams" : "free";
|
||||
}
|
||||
|
||||
const hmac = createHmac("sha256", secret);
|
||||
hmac.update(session.user.email.toLowerCase().trim());
|
||||
const hash = hmac.digest("hex");
|
||||
@@ -43,6 +73,7 @@ async function handler() {
|
||||
appId: process.env.NEXT_PUBLIC_PLAIN_CHAT_ID,
|
||||
fullName: session.user.name || "User",
|
||||
chatAvatarUrl: session.user.avatarUrl || "",
|
||||
userTier,
|
||||
});
|
||||
|
||||
return NextResponse.json(response);
|
||||
|
||||
@@ -72,7 +72,7 @@ const Container = z.object({
|
||||
containerContent: z.array(ContainerContentUnionInput).min(1),
|
||||
});
|
||||
|
||||
export const Component = z.object({
|
||||
export const CardComponent = z.object({
|
||||
componentText: Text.optional(),
|
||||
componentDivider: Divider.optional(),
|
||||
componentLinkButton: LinkButton.optional(),
|
||||
@@ -83,4 +83,4 @@ export const Component = z.object({
|
||||
componentContainer: Container.optional(),
|
||||
});
|
||||
|
||||
export type Component = z.infer<typeof Component>;
|
||||
export type CardComponent = z.infer<typeof CardComponent>;
|
||||
@@ -34,6 +34,12 @@ interface PlainChatConfig {
|
||||
chatButtons: Array<{
|
||||
icon: string;
|
||||
text: string;
|
||||
threadDetails?: {
|
||||
labelTypeIds: Array<string>;
|
||||
tierIdentifier: {
|
||||
externalId: string;
|
||||
};
|
||||
};
|
||||
form?: {
|
||||
fields: Array<{
|
||||
type: string;
|
||||
@@ -43,7 +49,11 @@ interface PlainChatConfig {
|
||||
text: string;
|
||||
threadDetails: {
|
||||
labelTypeIds: Array<string>;
|
||||
tierIdentifier: {
|
||||
externalId: string;
|
||||
};
|
||||
};
|
||||
onClick?: () => void;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
@@ -150,10 +160,18 @@ const PlainChat = () => {
|
||||
{
|
||||
icon: "chat",
|
||||
text: "Ask a question",
|
||||
threadDetails: {
|
||||
labelTypeIds: ["lt_01JFJWNWAC464N8DZ6YE71YJRF"],
|
||||
tierIdentifier: { externalId: data.userTier },
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "bulb",
|
||||
text: "Send feedback",
|
||||
threadDetails: {
|
||||
labelTypeIds: ["lt_01JFJWP3KECF1YQES6XF212RFW"],
|
||||
tierIdentifier: { externalId: data.userTier },
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "error",
|
||||
@@ -169,6 +187,10 @@ const PlainChat = () => {
|
||||
text: "I'm unable to use the app",
|
||||
threadDetails: {
|
||||
labelTypeIds: ["lt_01JFJWNWAC464N8DZ6YE71YJRF"],
|
||||
tierIdentifier: { externalId: data.userTier },
|
||||
},
|
||||
onClick: () => {
|
||||
console.log("User selected severity: Unable to use app");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -176,6 +198,10 @@ const PlainChat = () => {
|
||||
text: "Major functionality degraded",
|
||||
threadDetails: {
|
||||
labelTypeIds: ["lt_01JFJWP3KECF1YQES6XF212RFW"],
|
||||
tierIdentifier: { externalId: data.userTier },
|
||||
},
|
||||
onClick: () => {
|
||||
console.log("User selected severity: Major functionality degraded");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -183,6 +209,10 @@ const PlainChat = () => {
|
||||
text: "Minor annoyance",
|
||||
threadDetails: {
|
||||
labelTypeIds: ["lt_01JFJWPC8ADW0PK28JHMJR6NSS"],
|
||||
tierIdentifier: { externalId: data.userTier },
|
||||
},
|
||||
onClick: () => {
|
||||
console.log("User selected severity: Minor annoyance");
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,407 +0,0 @@
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
import type { Card } from "@pages/api/plain";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
export default (
|
||||
name: string,
|
||||
email: string,
|
||||
id: string,
|
||||
username: string,
|
||||
timeZone: string,
|
||||
emailVerified: Date | null,
|
||||
twoFactorEnabled: boolean | null,
|
||||
identityProvider: string | null,
|
||||
lastActiveAt: Date | null,
|
||||
teamName: string | null,
|
||||
teamSlug: string | null,
|
||||
isOrganization: boolean | null,
|
||||
stripeCustomerId: string | null
|
||||
): Card => {
|
||||
return {
|
||||
key: "customer-card",
|
||||
timeToLiveSeconds: null,
|
||||
components: [
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
text: "Basic Information",
|
||||
textColor: "NORMAL",
|
||||
textSize: "L",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Name",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: name || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: email || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Username",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: username || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "User ID",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: id || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: "M",
|
||||
},
|
||||
},
|
||||
// Account Settings Section Title
|
||||
{
|
||||
componentText: {
|
||||
text: "Account Settings",
|
||||
textColor: "NORMAL",
|
||||
textSize: "L",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Time Zone",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: timeZone || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Identity Provider",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: identityProvider || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Last Active At",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: lastActiveAt ? dayjs(lastActiveAt).fromNow() : "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: "M",
|
||||
},
|
||||
},
|
||||
// Team Information Section Title
|
||||
{
|
||||
componentText: {
|
||||
text: "Team Information",
|
||||
textColor: "NORMAL",
|
||||
textSize: "L",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Team Name",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: teamName || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Team Slug",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: teamSlug || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Is Organization",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: isOrganization ? "Yes" : "No",
|
||||
badgeColor: isOrganization ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
text: "Security & Billing",
|
||||
textColor: "NORMAL",
|
||||
textSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Stripe Customer ID",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: stripeCustomerId || "Unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Email Verified?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No",
|
||||
badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
componentSpacer: {
|
||||
spacerSize: "M",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentRow: {
|
||||
rowMainContent: [
|
||||
{
|
||||
componentText: {
|
||||
text: "Two Factor Enabled?",
|
||||
textColor: "MUTED",
|
||||
},
|
||||
},
|
||||
],
|
||||
rowAsideContent: [
|
||||
{
|
||||
componentBadge: {
|
||||
badgeLabel: twoFactorEnabled === null ? "No" : twoFactorEnabled ? "Yes" : "No",
|
||||
badgeColor: twoFactorEnabled === null ? "RED" : twoFactorEnabled ? "GREEN" : "RED",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { Card } from "@pages/api/plain";
|
||||
import customerCardDisplay from "@pages/api/plain/customer-card-display";
|
||||
|
||||
export const cardExamples: ((
|
||||
name: string,
|
||||
email: string,
|
||||
id: string,
|
||||
username: string,
|
||||
timeZone: string,
|
||||
emailVerified: Date | null,
|
||||
twoFactorEnabled: boolean | null,
|
||||
identityProvider: string | null,
|
||||
lastActiveAt: Date | null,
|
||||
teamName: string | null,
|
||||
teamSlug: string | null,
|
||||
isOrganization: boolean | null,
|
||||
stripeCustomerId: string | null
|
||||
) => Card)[] = [customerCardDisplay];
|
||||
@@ -1,15 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { Component } from "./components";
|
||||
|
||||
export const Card = z.object({
|
||||
key: z.string(),
|
||||
timeToLiveSeconds: z.number().int().min(0).nullish().default(null),
|
||||
components: z.array(Component).nullable(),
|
||||
});
|
||||
export type Card = z.infer<typeof Card>;
|
||||
|
||||
export const ResponseBody = z.object({
|
||||
cards: z.array(Card),
|
||||
});
|
||||
export type ResponseBody = z.infer<typeof ResponseBody>;
|
||||
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
||||
import { POST } from "../../app/api/plain-hash/route";
|
||||
|
||||
// Mock NextResponse
|
||||
vi.mock("next/server", () => ({
|
||||
NextResponse: {
|
||||
json: (data: any, init?: ResponseInit) => ({
|
||||
json: async () => data,
|
||||
status: init?.status || 200,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock session
|
||||
vi.mock("@calcom/features/auth/lib/getServerSession", () => ({
|
||||
getServerSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/headers", () => ({
|
||||
headers: () => new Headers(),
|
||||
cookies: () => ({ get: () => null }),
|
||||
}));
|
||||
|
||||
vi.mock("@lib/buildLegacyCtx", () => ({
|
||||
buildLegacyRequest: vi.fn().mockReturnValue({
|
||||
headers: new Headers(),
|
||||
cookies: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("app/api/apiRouteMiddleware", () => ({
|
||||
apiRouteMiddleware: (handler: () => Promise<Response>) => handler,
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/prisma", () => ({
|
||||
default: prismaMock,
|
||||
}));
|
||||
|
||||
describe("Plain Integration API", () => {
|
||||
beforeAll(() => {
|
||||
process.env.PLAIN_CHAT_HMAC_SECRET_KEY = "test-secret";
|
||||
process.env.NEXT_PUBLIC_PLAIN_CHAT_ID = "test-chat-id";
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
const tiers = [
|
||||
{ tier: "free", teams: [] },
|
||||
{ tier: "teams", teams: [{ team: { id: 2 } }] },
|
||||
{ tier: "enterprise", teams: [{ team: { id: 3, isOrganization: true } }] },
|
||||
];
|
||||
|
||||
it.each(tiers)("should return $tier tier", async ({ tier, teams }) => {
|
||||
const userData = { id: 1, email: `${tier}@example.com`, name: `${tier} User` };
|
||||
(getServerSession as any).mockResolvedValue({ user: userData });
|
||||
prismaMock.user.findUnique.mockResolvedValue({ ...userData, teams });
|
||||
|
||||
const data = await (await POST({} as NextRequest, { params: {} })).json();
|
||||
expect(data).toMatchObject({ userTier: tier, email: userData.email, fullName: userData.name });
|
||||
});
|
||||
|
||||
it("should return 401 when no session exists", async () => {
|
||||
(getServerSession as any).mockResolvedValue(null);
|
||||
expect((await POST({} as NextRequest, { params: {} })).status).toBe(401);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user