fix: paypal setup and key validation on server (#10761)
* fixes: paypal setup and key validation on server * Renamed for legiblity * Prefer early returns * Update getEnabledAppsFromCredentials.ts * Updated getEnabledAppsFromCredentials references * Update getEnabledAppsFromCredentials.ts
This commit is contained in:
@@ -3,7 +3,7 @@ import { useState } from "react";
|
||||
|
||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
import { currencyOptions } from "@calcom/app-store/paypal/pages/setup";
|
||||
import { currencyOptions } from "@calcom/app-store/paypal/lib/currencyOptions";
|
||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
@@ -12,12 +12,25 @@ class Paypal {
|
||||
accessToken: string | null = null;
|
||||
expiresAt: number | null = null;
|
||||
|
||||
constructor({ clientId, secretKey }: { clientId: string; secretKey: string }) {
|
||||
constructor(opts: { clientId: string; secretKey: string }) {
|
||||
this.url = IS_PRODUCTION ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com";
|
||||
this.clientId = clientId;
|
||||
this.secretKey = secretKey;
|
||||
this.clientId = opts.clientId;
|
||||
this.secretKey = opts.secretKey;
|
||||
}
|
||||
|
||||
private fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
|
||||
this.getAccessToken();
|
||||
return fetch(`${this.url}${endpoint}`, {
|
||||
method: "get",
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async getAccessToken(): Promise<void> {
|
||||
if (this.accessToken && this.expiresAt && this.expiresAt > Date.now()) {
|
||||
return;
|
||||
@@ -64,15 +77,6 @@ class Paypal {
|
||||
cancelUrl: string;
|
||||
intent?: "CAPTURE" | "AUTHORIZE";
|
||||
}): Promise<CreateOrderResponse> {
|
||||
// Always get a new access token
|
||||
await this.getAccessToken();
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
"PayPal-Request-Id": uuidv4(),
|
||||
};
|
||||
|
||||
const createOrderRequestBody: CreateOrderRequestBody = {
|
||||
intent,
|
||||
purchase_units: [
|
||||
@@ -96,9 +100,11 @@ class Paypal {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.url}/v2/checkout/orders`, {
|
||||
const response = await this.fetcher("/v2/checkout/orders", {
|
||||
method: "POST",
|
||||
headers,
|
||||
headers: {
|
||||
"PayPal-Request-Id": uuidv4(),
|
||||
},
|
||||
body: JSON.stringify(createOrderRequestBody),
|
||||
});
|
||||
|
||||
@@ -116,16 +122,8 @@ class Paypal {
|
||||
|
||||
async captureOrder(orderId: string): Promise<boolean> {
|
||||
try {
|
||||
await this.getAccessToken();
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
};
|
||||
|
||||
const captureResult = await fetch(`${this.url}/v2/checkout/orders/${orderId}/capture`, {
|
||||
const captureResult = await this.fetcher(`/v2/checkout/orders/${orderId}/capture`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
});
|
||||
if (captureResult.ok) {
|
||||
const result = await captureResult.json();
|
||||
@@ -181,14 +179,6 @@ class Paypal {
|
||||
}
|
||||
|
||||
async createWebhook(): Promise<boolean | string> {
|
||||
// Always get a new access token
|
||||
await this.getAccessToken();
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
};
|
||||
|
||||
const body = {
|
||||
url: `${WEBAPP_URL}/api/integrations/paypal/webhook`,
|
||||
event_types: [
|
||||
@@ -202,9 +192,8 @@ class Paypal {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.url}/v1/notifications/webhooks`, {
|
||||
const response = await this.fetcher(`/v1/notifications/webhooks`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
@@ -220,19 +209,8 @@ class Paypal {
|
||||
}
|
||||
|
||||
async listWebhooks(): Promise<string[]> {
|
||||
// Always get a new access token
|
||||
await this.getAccessToken();
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.url}/v1/notifications/webhooks`, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
const response = await this.fetcher(`/v1/notifications/webhooks`);
|
||||
|
||||
if (response.ok) {
|
||||
const { webhooks } = await response.json();
|
||||
@@ -251,18 +229,9 @@ class Paypal {
|
||||
}
|
||||
|
||||
async deleteWebhook(webhookId: string): Promise<boolean> {
|
||||
// Always get a new access token
|
||||
await this.getAccessToken();
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.url}/v1/notifications/webhooks/${webhookId}`, {
|
||||
const response = await this.fetcher(`/v1/notifications/webhooks/${webhookId}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -286,7 +255,6 @@ class Paypal {
|
||||
}
|
||||
|
||||
async verifyWebhook(options: WebhookEventVerifyRequest): Promise<void> {
|
||||
const url = `${this.url}/v1/notifications/verify-webhook-signature`;
|
||||
const parseRequest = webhookEventVerifyRequestSchema.safeParse(options);
|
||||
|
||||
// Webhook event should be parsable
|
||||
@@ -307,12 +275,8 @@ class Paypal {
|
||||
const bodyToString = stringy.slice(0, -1) + `,"webhook_event":${options.body.webhook_event}` + "}";
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await this.fetcher(`/v1/notifications/verify-webhook-signature`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
},
|
||||
body: bodyToString,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export const currencyOptions = [
|
||||
{ label: "United States dollar", value: "USD" },
|
||||
{ label: "Australian dollar", value: "AUD" },
|
||||
{ label: "Brazilian real 2", value: "BRL" },
|
||||
{ label: "Canadian dollar", value: "CAD" },
|
||||
{ label: "Chinese Renmenbi 3", value: "CNY" },
|
||||
{ label: "Czech koruna", value: "CZK" },
|
||||
{ label: "Danish krone", value: "DKK" },
|
||||
{ label: "Euro", value: "EUR" },
|
||||
{ label: "Hong Kong dollar", value: "HKD" },
|
||||
{ label: "Hungarian forint 1", value: "HUF" },
|
||||
{ label: "Israeli new shekel", value: "ILS" },
|
||||
{ label: "Japanese yen 1", value: "JPY" },
|
||||
{ label: "Malaysian ringgit 3", value: "MYR" },
|
||||
{ label: "Mexican peso", value: "MXN" },
|
||||
{ label: "New Taiwan dollar 1", value: "TWD" },
|
||||
{ label: "New Zealand dollar", value: "NZD" },
|
||||
{ label: "Norwegian krone", value: "NOK" },
|
||||
{ label: "Philippine peso", value: "PHP" },
|
||||
{ label: "Polish złoty", value: "PLN" },
|
||||
{ label: "Pound sterling", value: "GBP" },
|
||||
{ label: "Russian ruble", value: "RUB" },
|
||||
{ label: "Singapore dollar", value: "SGD" },
|
||||
{ label: "Swedish krona", value: "SEK" },
|
||||
{ label: "Swiss franc", value: "CHF" },
|
||||
{ label: "Thai baht", value: "THB" },
|
||||
];
|
||||
@@ -0,0 +1,47 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { UpdateAppCredentialsOptions } from "@calcom/trpc/server/routers/viewer/apps/updateAppCredentials.handler";
|
||||
|
||||
import Paypal from "./Paypal";
|
||||
|
||||
const schema = z.object({
|
||||
credentialId: z.coerce.number(),
|
||||
key: z.object({
|
||||
client_id: z.string(),
|
||||
secret_key: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const handlePaypalValidations = async ({ input }: UpdateAppCredentialsOptions) => {
|
||||
const validated = schema.safeParse(input);
|
||||
if (!validated.success) throw new Error("Invalid input");
|
||||
const { key } = validated.data;
|
||||
|
||||
// Test credentials before saving
|
||||
const paypalClient = new Paypal({ clientId: key.client_id, secretKey: key.secret_key });
|
||||
const test = await paypalClient.test();
|
||||
if (!test) throw new Error("Provided credentials failed to authenticate");
|
||||
|
||||
// Delete all existing webhooks
|
||||
const webhooksToDelete = await paypalClient.listWebhooks();
|
||||
if (webhooksToDelete) {
|
||||
const promises = webhooksToDelete.map((webhook) => paypalClient.deleteWebhook(webhook));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
// Create webhook for this installation
|
||||
const webhookId = await paypalClient.createWebhook();
|
||||
if (!webhookId) {
|
||||
// @TODO: make a button that tries to create the webhook again
|
||||
console.error("Failed to create webhook", webhookId);
|
||||
throw new Error("Failed to create webhook");
|
||||
}
|
||||
|
||||
return {
|
||||
client_id: key.client_id,
|
||||
secret_key: key.secret_key,
|
||||
webhook_id: webhookId,
|
||||
};
|
||||
};
|
||||
|
||||
export default handlePaypalValidations;
|
||||
@@ -1,57 +1,20 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import Paypal from "@calcom/app-store/paypal/lib/Paypal";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Button, showToast, TextField } from "@calcom/ui";
|
||||
|
||||
export interface PayPalSetupProps {
|
||||
public_key: string;
|
||||
access_token: string;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export const currencyOptions = [
|
||||
{ label: "United States dollar", value: "USD" },
|
||||
{ label: "Australian dollar", value: "AUD" },
|
||||
{ label: "Brazilian real 2", value: "BRL" },
|
||||
{ label: "Canadian dollar", value: "CAD" },
|
||||
{ label: "Chinese Renmenbi 3", value: "CNY" },
|
||||
{ label: "Czech koruna", value: "CZK" },
|
||||
{ label: "Danish krone", value: "DKK" },
|
||||
{ label: "Euro", value: "EUR" },
|
||||
{ label: "Hong Kong dollar", value: "HKD" },
|
||||
{ label: "Hungarian forint 1", value: "HUF" },
|
||||
{ label: "Israeli new shekel", value: "ILS" },
|
||||
{ label: "Japanese yen 1", value: "JPY" },
|
||||
{ label: "Malaysian ringgit 3", value: "MYR" },
|
||||
{ label: "Mexican peso", value: "MXN" },
|
||||
{ label: "New Taiwan dollar 1", value: "TWD" },
|
||||
{ label: "New Zealand dollar", value: "NZD" },
|
||||
{ label: "Norwegian krone", value: "NOK" },
|
||||
{ label: "Philippine peso", value: "PHP" },
|
||||
{ label: "Polish złoty", value: "PLN" },
|
||||
{ label: "Pound sterling", value: "GBP" },
|
||||
{ label: "Russian ruble", value: "RUB" },
|
||||
{ label: "Singapore dollar", value: "SGD" },
|
||||
{ label: "Swedish krona", value: "SEK" },
|
||||
{ label: "Swiss franc", value: "CHF" },
|
||||
{ label: "Thai baht", value: "THB" },
|
||||
];
|
||||
|
||||
export default function PayPalSetup(props: PayPalSetupProps) {
|
||||
export default function PayPalSetup() {
|
||||
const [newClientId, setNewClientId] = useState("");
|
||||
const [newSecretKey, setNewSecretKey] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
|
||||
const integrations = trpc.viewer.integrations.useQuery({ variant: "payment" });
|
||||
const paypalPaymentAppCredentials = integrations.data?.items.find((item) => item.type === "paypal_payment");
|
||||
const [credentialId] = paypalPaymentAppCredentials?.userCredentialIds || [false];
|
||||
const integrations = trpc.viewer.integrations.useQuery({ variant: "payment", appId: "paypal" });
|
||||
const [paypalPaymentAppCredentials] = integrations.data?.items || [];
|
||||
const [credentialId] = paypalPaymentAppCredentials?.userCredentialIds || [-1];
|
||||
const showContent = !!integrations.data && integrations.isSuccess && !!credentialId;
|
||||
const saveKeysMutation = trpc.viewer.appsRouter.updateAppCredentials.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -63,48 +26,6 @@ export default function PayPalSetup(props: PayPalSetupProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const saveKeys = async (key: { clientId: string; secretKey: string }) => {
|
||||
if (!key.clientId || !key.secretKey || key.clientId === key.secretKey) {
|
||||
return false;
|
||||
}
|
||||
if (typeof credentialId !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Test credentials before saving
|
||||
const paypalClient = new Paypal({ clientId: key.clientId, secretKey: key.secretKey });
|
||||
const test = await paypalClient.test();
|
||||
if (!test) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete all existing webhooks
|
||||
const webhooksToDelete = await paypalClient.listWebhooks();
|
||||
if (webhooksToDelete) {
|
||||
for (const webhook of webhooksToDelete) {
|
||||
await paypalClient.deleteWebhook(webhook);
|
||||
}
|
||||
}
|
||||
|
||||
// Create webhook for this installation
|
||||
const webhookId = await paypalClient.createWebhook();
|
||||
if (!webhookId) {
|
||||
// @TODO: make a button that tries to create the webhook again
|
||||
console.log("Failed to create webhook", webhookId);
|
||||
return false;
|
||||
}
|
||||
saveKeysMutation.mutate({
|
||||
credentialId,
|
||||
key: {
|
||||
client_id: key.clientId,
|
||||
secret_key: key.secretKey,
|
||||
webhook_id: webhookId,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (integrations.isLoading) {
|
||||
return <div className="absolute z-50 flex h-screen w-full items-center bg-gray-200" />;
|
||||
}
|
||||
@@ -144,12 +65,15 @@ export default function PayPalSetup(props: PayPalSetupProps) {
|
||||
<div className="mt-5 flex flex-row justify-end">
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
saveKeys({
|
||||
clientId: newClientId,
|
||||
secretKey: newSecretKey,
|
||||
})
|
||||
}>
|
||||
onClick={() => {
|
||||
saveKeysMutation.mutate({
|
||||
credentialId,
|
||||
key: {
|
||||
client_id: newClientId,
|
||||
secret_key: newSecretKey,
|
||||
},
|
||||
});
|
||||
}}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TFunction } from "next-i18next";
|
||||
|
||||
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
|
||||
import { defaultVideoAppCategories } from "@calcom/app-store/utils";
|
||||
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function getLocationGroupedOptions(
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = await getEnabledApps(credentials, true);
|
||||
const integrations = await getEnabledAppsFromCredentials(credentials, { filterOnCredentials: true });
|
||||
|
||||
integrations.forEach((app) => {
|
||||
// All apps that are labeled as a locationOption are video apps.
|
||||
|
||||
+22
-11
@@ -1,18 +1,27 @@
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import type { Prisma } from ".prisma/client";
|
||||
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
type EnabledApp = ReturnType<typeof getApps>[number] & { enabled: boolean };
|
||||
|
||||
/**
|
||||
*
|
||||
* @param credentials - Can be user or team credentials
|
||||
* @param filterOnCredentials - Only include apps where credentials are present
|
||||
* @param options
|
||||
* @param options.where Aditional where conditions to filter out apps
|
||||
* @param options.filterOnCredentials - Only include apps where credentials are present
|
||||
* @returns A list of enabled app metadata & credentials tied to them
|
||||
*/
|
||||
const getEnabledApps = async (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => {
|
||||
const getEnabledAppsFromCredentials = async (
|
||||
credentials: CredentialDataWithTeamName[],
|
||||
options?: {
|
||||
where?: Prisma.AppWhereInput;
|
||||
filterOnCredentials?: boolean;
|
||||
}
|
||||
) => {
|
||||
const { where: _where = {}, filterOnCredentials = false } = options || {};
|
||||
const filterOnIds = {
|
||||
credentials: {
|
||||
some: {
|
||||
@@ -33,11 +42,13 @@ const getEnabledApps = async (credentials: CredentialDataWithTeamName[], filterO
|
||||
if (teamIds.length) filterOnIds.credentials.some.OR.push({ teamId: { in: teamIds } });
|
||||
}
|
||||
|
||||
const where: Prisma.AppWhereInput = {
|
||||
enabled: true,
|
||||
..._where,
|
||||
...(filterOnIds.credentials.some.OR.length && filterOnIds),
|
||||
};
|
||||
const enabledApps = await prisma.app.findMany({
|
||||
where: {
|
||||
enabled: true,
|
||||
...(filterOnIds.credentials.some.OR.length && filterOnIds),
|
||||
},
|
||||
where,
|
||||
select: { slug: true, enabled: true },
|
||||
});
|
||||
const apps = getApps(credentials, filterOnCredentials);
|
||||
@@ -52,4 +63,4 @@ const getEnabledApps = async (credentials: CredentialDataWithTeamName[], filterO
|
||||
return filteredApps;
|
||||
};
|
||||
|
||||
export default getEnabledApps;
|
||||
export default getEnabledAppsFromCredentials;
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Credential, Prisma } from "@prisma/client";
|
||||
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
|
||||
import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp";
|
||||
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -7,7 +9,6 @@ import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import type { TIntegrationsInputSchema } from "./integrations.schema";
|
||||
import type { Prisma, Credential } from ".prisma/client";
|
||||
|
||||
type IntegrationsOptions = {
|
||||
ctx: {
|
||||
@@ -44,6 +45,8 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
|
||||
extendsFeature,
|
||||
teamId,
|
||||
sortByMostPopular,
|
||||
categories,
|
||||
appId,
|
||||
} = input;
|
||||
let credentials = await getUsersCredentials(user.id);
|
||||
let userTeams: TeamQuery[] = [];
|
||||
@@ -116,7 +119,10 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
|
||||
}
|
||||
}
|
||||
|
||||
const enabledApps = await getEnabledApps(credentials, onlyInstalled);
|
||||
const enabledApps = await getEnabledAppsFromCredentials(credentials, {
|
||||
filterOnCredentials: onlyInstalled,
|
||||
...(appId ? { where: { slug: appId } } : {}),
|
||||
});
|
||||
//TODO: Refactor this to pick up only needed fields and prevent more leaking
|
||||
let apps = enabledApps.map(
|
||||
({ credentials: _, credential: _1, key: _2 /* don't leak to frontend */, ...app }) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
export const ZIntegrationsInputSchema = z.object({
|
||||
variant: z.string().optional(),
|
||||
exclude: z.array(z.string()).optional(),
|
||||
@@ -8,6 +10,8 @@ export const ZIntegrationsInputSchema = z.object({
|
||||
extendsFeature: z.literal("EventType").optional(),
|
||||
teamId: z.union([z.number(), z.null()]).optional(),
|
||||
sortByMostPopular: z.boolean().optional(),
|
||||
categories: z.nativeEnum(AppCategories).array().optional(),
|
||||
appId: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TIntegrationsInputSchema = z.infer<typeof ZIntegrationsInputSchema>;
|
||||
|
||||
@@ -5,8 +5,9 @@ import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import type { TUpdateAppCredentialsInputSchema } from "./updateAppCredentials.schema";
|
||||
import { handleCustomValidations } from "./updateAppCredentials.validator";
|
||||
|
||||
type UpdateAppCredentialsOptions = {
|
||||
export type UpdateAppCredentialsOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
@@ -16,8 +17,6 @@ type UpdateAppCredentialsOptions = {
|
||||
export const updateAppCredentialsHandler = async ({ ctx, input }: UpdateAppCredentialsOptions) => {
|
||||
const { user } = ctx;
|
||||
|
||||
const { key } = input;
|
||||
|
||||
// Find user credential
|
||||
const credential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
@@ -33,6 +32,8 @@ export const updateAppCredentialsHandler = async ({ ctx, input }: UpdateAppCrede
|
||||
});
|
||||
}
|
||||
|
||||
const validatedKeys = await handleCustomValidations({ ctx, input, appId: credential.appId || "" });
|
||||
|
||||
const updated = await prisma.credential.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
@@ -40,7 +41,7 @@ export const updateAppCredentialsHandler = async ({ ctx, input }: UpdateAppCrede
|
||||
data: {
|
||||
key: {
|
||||
...(credential.key as Prisma.JsonObject),
|
||||
...(key as Prisma.JsonObject),
|
||||
...(validatedKeys as Prisma.JsonObject),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { UpdateAppCredentialsOptions } from "./updateAppCredentials.handler";
|
||||
|
||||
const validators = {
|
||||
paypal: () => import("@calcom/paypal/lib/updateAppCredentials.validator"),
|
||||
};
|
||||
|
||||
export const handleCustomValidations = async ({
|
||||
ctx,
|
||||
input,
|
||||
appId,
|
||||
}: UpdateAppCredentialsOptions & { appId: string }) => {
|
||||
const { key } = input;
|
||||
const validatorGetter = validators[appId as keyof typeof validators];
|
||||
// If no validator is found, return the key as is
|
||||
if (!validatorGetter) return key;
|
||||
try {
|
||||
const validator = (await validatorGetter()).default;
|
||||
return await validator({ input, ctx });
|
||||
} catch (error) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user