-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* revert: "fix: Request permissions to allow events to be created on shared Office365/Outlook calendars (#17760)" This reverts commit 1680cba. * plain.com card * detiled error handling for get customer * working email and id * hmac * Revert "fix: correct line-breaks in calendar event description (#18077)" This reverts commit 06494a6. * pr changes requet * remove pan for now * add-new-implementation-for-early-review * Pushing fix for createHmac stringify * added validation and user repository for email check * add apiRouteMiddleware which handles the error handling * HMAC_SECRET_KEY -> PLAIN_HMAC_SECRET_KEY * Use the right error * Convey right error to consumer * Fixup apiRouteMiddleware to handle handler * Don't export handler, only export POST * changed to app directory * working unkown user card --------- Co-authored-by: Keith Williams <[email protected]> Co-authored-by: Omar López <[email protected]> Co-authored-by: Alex van Andel <[email protected]>
- Loading branch information
1 parent
6b3ccbc
commit 63013d4
Showing
10 changed files
with
7,455 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
import { cardExamples } from "@pages/api/plain/example-cards"; | ||
import { createHmac } from "crypto"; | ||
import { headers } from "next/headers"; | ||
import { NextResponse } from "next/server"; | ||
import { z } from "zod"; | ||
|
||
import { apiRouteMiddleware } from "@calcom/lib/server/apiRouteMiddleware"; | ||
import { UserRepository } from "@calcom/lib/server/repository/user"; | ||
import { userMetadata } from "@calcom/prisma/zod-utils"; | ||
|
||
const inputSchema = z.object({ | ||
customer: z.object({ | ||
email: z.string().email(), | ||
username: z.string().optional(), | ||
timeZone: z.string().optional(), | ||
emailVerified: z.boolean().optional(), | ||
identityProvider: z.string().optional(), | ||
twoFactorEnabled: z.boolean().optional(), | ||
}), | ||
cardKeys: z.array(z.string()), | ||
}); | ||
|
||
export async function handler(request: Request) { | ||
const headersList = headers(); | ||
const requestBody = await request.json(); | ||
|
||
// HMAC verification | ||
const incomingSignature = headersList.get("plain-request-signature"); | ||
const expectedSignature = createHmac("sha-256", process.env.PLAIN_HMAC_SECRET_KEY!) | ||
.update(JSON.stringify(requestBody)) | ||
.digest("hex"); | ||
|
||
if (incomingSignature !== expectedSignature) { | ||
return new Response("Forbidden", { status: 403 }); | ||
} | ||
|
||
// Validate request body | ||
const { cardKeys, customer } = inputSchema.parse(requestBody); | ||
|
||
const user = await UserRepository.findByEmail({ email: customer.email }); | ||
|
||
if (!user) { | ||
return NextResponse.json({ | ||
cards: [ | ||
{ | ||
key: "customer-card", | ||
timeToLiveSeconds: null, | ||
components: [ | ||
{ | ||
componentSpacer: { | ||
spacerSize: "M", | ||
}, | ||
}, | ||
{ | ||
componentRow: { | ||
rowMainContent: [ | ||
{ | ||
componentText: { | ||
text: "Email", | ||
textColor: "MUTED", | ||
}, | ||
}, | ||
], | ||
rowAsideContent: [ | ||
{ | ||
componentText: { | ||
text: customer.email || "Unknown", | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
{ | ||
componentSpacer: { | ||
spacerSize: "M", | ||
}, | ||
}, | ||
{ | ||
componentRow: { | ||
rowMainContent: [ | ||
{ | ||
componentText: { | ||
text: "Email Verified?", | ||
textColor: "MUTED", | ||
}, | ||
}, | ||
], | ||
rowAsideContent: [ | ||
{ | ||
componentBadge: { | ||
badgeLabel: | ||
customer.emailVerified === undefined | ||
? "Unknown" | ||
: customer.emailVerified | ||
? "Yes" | ||
: "No", | ||
badgeColor: | ||
customer.emailVerified === undefined | ||
? "YELLOW" | ||
: customer.emailVerified | ||
? "GREEN" | ||
: "RED", | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
{ | ||
componentSpacer: { | ||
spacerSize: "M", | ||
}, | ||
}, | ||
{ | ||
componentRow: { | ||
rowMainContent: [ | ||
{ | ||
componentText: { | ||
text: "Username", | ||
textColor: "MUTED", | ||
}, | ||
}, | ||
], | ||
rowAsideContent: [ | ||
{ | ||
componentText: { | ||
text: customer.username || "Unknown", | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
{ | ||
componentSpacer: { | ||
spacerSize: "M", | ||
}, | ||
}, | ||
{ | ||
componentRow: { | ||
rowMainContent: [ | ||
{ | ||
componentText: { | ||
text: "User ID", | ||
textColor: "MUTED", | ||
}, | ||
}, | ||
], | ||
rowAsideContent: [ | ||
{ | ||
componentText: { | ||
text: "Unknown", | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
{ | ||
componentSpacer: { | ||
spacerSize: "M", | ||
}, | ||
}, | ||
{ | ||
componentRow: { | ||
rowMainContent: [ | ||
{ | ||
componentText: { | ||
text: "Time Zone", | ||
textColor: "MUTED", | ||
}, | ||
}, | ||
], | ||
rowAsideContent: [ | ||
{ | ||
componentText: { | ||
text: customer.timeZone || "Unknown", | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
{ | ||
componentSpacer: { | ||
spacerSize: "M", | ||
}, | ||
}, | ||
{ | ||
componentRow: { | ||
rowMainContent: [ | ||
{ | ||
componentText: { | ||
text: "Two Factor Enabled?", | ||
textColor: "MUTED", | ||
}, | ||
}, | ||
], | ||
rowAsideContent: [ | ||
{ | ||
componentBadge: { | ||
badgeLabel: | ||
customer.twoFactorEnabled === undefined | ||
? "Unknown" | ||
: customer.twoFactorEnabled | ||
? "Yes" | ||
: "No", | ||
badgeColor: | ||
customer.twoFactorEnabled === undefined | ||
? "YELLOW" | ||
: customer.twoFactorEnabled | ||
? "GREEN" | ||
: "RED", | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
{ | ||
componentSpacer: { | ||
spacerSize: "M", | ||
}, | ||
}, | ||
{ | ||
componentRow: { | ||
rowMainContent: [ | ||
{ | ||
componentText: { | ||
text: "Identity Provider", | ||
textColor: "MUTED", | ||
}, | ||
}, | ||
], | ||
rowAsideContent: [ | ||
{ | ||
componentText: { | ||
text: customer.identityProvider || "Unknown", | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
}); | ||
} | ||
|
||
// Parse user metadata | ||
const parsedMetadata = userMetadata.parse(user.metadata); | ||
|
||
const cards = await Promise.all( | ||
cardExamples.map(async (cardFn) => { | ||
return cardFn( | ||
user.email, | ||
user.id.toString(), | ||
user.username || "Unknown", | ||
user.timeZone, | ||
user.emailVerified, | ||
user.twoFactorEnabled | ||
); | ||
}) | ||
); | ||
|
||
const filteredCards = cards.filter((card) => { | ||
return cardKeys.length === 0 || cardKeys.includes(card.key); | ||
}); | ||
|
||
return NextResponse.json({ | ||
cards: filteredCards, | ||
user: { | ||
...user, | ||
metadata: parsedMetadata, | ||
}, | ||
}); | ||
} | ||
|
||
export const POST = apiRouteMiddleware(handler); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { z } from "zod"; | ||
|
||
export const ComponentTextSize = z.enum(["S", "M", "L"]); | ||
export type ComponentTextSize = z.infer<typeof ComponentTextSize>; | ||
|
||
export const ComponentTextColor = z.enum(["NORMAL", "MUTED", "SUCCESS", "WARNING", "ERROR"]); | ||
export type ComponentTextColor = z.infer<typeof ComponentTextColor>; | ||
|
||
export const ComponentSpacerSize = z.enum(["XS", "S", "M", "L", "XL"]); | ||
export type ComponentSpacerSize = z.infer<typeof ComponentSpacerSize>; | ||
|
||
export const ComponentDividerSpacingSize = z.enum(["XS", "S", "M", "L", "XL"]); | ||
export type ComponentDividerSpacingSize = z.infer<typeof ComponentDividerSpacingSize>; | ||
|
||
export const ComponentBadgeColor = z.enum(["GREY", "GREEN", "YELLOW", "RED", "BLUE"]); | ||
export type ComponentBadgeColor = z.infer<typeof ComponentBadgeColor>; | ||
|
||
const Text = z.object({ | ||
textSize: ComponentTextSize.nullish(), | ||
textColor: ComponentTextColor.nullish(), | ||
text: z.string().min(1).max(5000), | ||
}); | ||
|
||
const Divider = z.object({ | ||
dividerSpacingSize: ComponentDividerSpacingSize.nullish(), | ||
}); | ||
|
||
const LinkButton = z.object({ | ||
linkButtonUrl: z.string().url(), | ||
linkButtonLabel: z.string().max(500), | ||
}); | ||
|
||
const Spacer = z.object({ | ||
spacerSize: ComponentSpacerSize, | ||
}); | ||
|
||
const Badge = z.object({ | ||
badgeLabel: z.string().max(500), | ||
badgeColor: ComponentBadgeColor.nullish(), | ||
}); | ||
|
||
const CopyButton = z.object({ | ||
copyButtonValue: z.string().max(1000), | ||
copyButtonTooltipLabel: z.string().max(500).nullish(), | ||
}); | ||
|
||
const RowContentUnionInput = z.object({ | ||
componentText: Text.optional(), | ||
componentDivider: Divider.optional(), | ||
componentLinkButton: LinkButton.optional(), | ||
componentSpacer: Spacer.optional(), | ||
componentBadge: Badge.optional(), | ||
componentCopyButton: CopyButton.optional(), | ||
}); | ||
|
||
const Row = z.object({ | ||
rowMainContent: z.array(RowContentUnionInput), | ||
rowAsideContent: z.array(RowContentUnionInput), | ||
}); | ||
|
||
const ContainerContentUnionInput = z.object({ | ||
componentText: Text.optional(), | ||
componentDivider: Divider.optional(), | ||
componentLinkButton: LinkButton.optional(), | ||
componentSpacer: Spacer.optional(), | ||
componentBadge: Badge.optional(), | ||
componentCopyButton: CopyButton.optional(), | ||
componentRow: Row.optional(), | ||
}); | ||
|
||
const Container = z.object({ | ||
containerContent: z.array(ContainerContentUnionInput).min(1), | ||
}); | ||
|
||
export const Component = z.object({ | ||
componentText: Text.optional(), | ||
componentDivider: Divider.optional(), | ||
componentLinkButton: LinkButton.optional(), | ||
componentSpacer: Spacer.optional(), | ||
componentBadge: Badge.optional(), | ||
componentCopyButton: CopyButton.optional(), | ||
componentRow: Row.optional(), | ||
componentContainer: Container.optional(), | ||
}); | ||
|
||
export type Component = z.infer<typeof Component>; |
Oops, something went wrong.