Merge branch 'main' into test/msw-gcal
commit
6fa2726537
15
.env.example
15
.env.example
|
@ -230,6 +230,19 @@ AUTH_BEARER_TOKEN_VERCEL=
|
|||
E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||
|
||||
# - APP CREDENTIAL SYNC ***********************************************************************************
|
||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
CALCOM_WEBHOOK_SECRET=""
|
||||
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
|
||||
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
|
||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||
# Key should match on Cal.com and your application
|
||||
# must be 32 bytes for AES256 encryption algorithm
|
||||
# You can use: `openssl rand -base64 24` to generate one
|
||||
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""
|
||||
|
||||
# - OIDC E2E TEST *******************************************************************************************
|
||||
|
||||
# Ensure this ADMIN EMAIL is present in the SAML_ADMINS list
|
||||
|
@ -243,4 +256,4 @@ E2E_TEST_OIDC_PROVIDER_DOMAIN=
|
|||
E2E_TEST_OIDC_USER_EMAIL=
|
||||
E2E_TEST_OIDC_USER_PASSWORD=
|
||||
|
||||
# ***********************************************************************************************************
|
||||
# ***********************************************************************************************************
|
||||
|
|
|
@ -226,6 +226,10 @@ const nextConfig = {
|
|||
},
|
||||
async rewrites() {
|
||||
const beforeFiles = [
|
||||
{
|
||||
source: "/login",
|
||||
destination: "/auth/login",
|
||||
},
|
||||
// These rewrites are other than booking pages rewrites and so that they aren't redirected to org pages ensure that they happen in beforeFiles
|
||||
...(isOrganizationsEnabled
|
||||
? [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.3.2",
|
||||
"version": "3.3.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
|
|
@ -130,6 +130,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
// Accept any child team invites for orgs and create a membership for the org itself
|
||||
if (team.parentId) {
|
||||
// Create (when invite link is used) or Update (when regular email invitation is used) membership for the organization itself
|
||||
await prisma.membership.upsert({
|
||||
where: {
|
||||
userId_teamId: { userId: user.id, teamId: team.parentId },
|
||||
},
|
||||
update: {
|
||||
accepted: true,
|
||||
},
|
||||
create: {
|
||||
userId: user.id,
|
||||
teamId: team.parentId,
|
||||
accepted: true,
|
||||
role: MembershipRole.MEMBER,
|
||||
},
|
||||
});
|
||||
|
||||
// We do a membership update twice so we can join the ORG invite if the user is invited to a team witin a ORG
|
||||
await prisma.membership.updateMany({
|
||||
where: {
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const appCredentialWebhookRequestBodySchema = z.object({
|
||||
// UserId of the cal.com user
|
||||
userId: z.number().int(),
|
||||
appSlug: z.string(),
|
||||
// Keys should be AES256 encrypted with the CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY
|
||||
keys: z.string(),
|
||||
});
|
||||
/** */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Check that credential sharing is enabled
|
||||
if (!APP_CREDENTIAL_SHARING_ENABLED) {
|
||||
return res.status(403).json({ message: "Credential sharing is not enabled" });
|
||||
}
|
||||
|
||||
// Check that the webhook secret matches
|
||||
if (
|
||||
req.headers[process.env.CALCOM_WEBHOOK_HEADER_NAME || "calcom-webhook-secret"] !==
|
||||
process.env.CALCOM_WEBHOOK_SECRET
|
||||
) {
|
||||
return res.status(403).json({ message: "Invalid webhook secret" });
|
||||
}
|
||||
|
||||
const reqBody = appCredentialWebhookRequestBodySchema.parse(req.body);
|
||||
|
||||
// Check that the user exists
|
||||
const user = await prisma.user.findUnique({ where: { id: reqBody.userId } });
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: "User not found" });
|
||||
}
|
||||
|
||||
const app = await prisma.app.findUnique({
|
||||
where: { slug: reqBody.appSlug },
|
||||
select: { slug: true },
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
return res.status(404).json({ message: "App not found" });
|
||||
}
|
||||
|
||||
// Search for the app's slug and type
|
||||
const appMetadata = appStoreMetadata[app.slug as keyof typeof appStoreMetadata];
|
||||
|
||||
if (!appMetadata) {
|
||||
return res.status(404).json({ message: "App not found. Ensure that you have the correct app slug" });
|
||||
}
|
||||
|
||||
// Decrypt the keys
|
||||
const keys = JSON.parse(
|
||||
symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
|
||||
);
|
||||
|
||||
// Can't use prisma upsert as we don't know the id of the credential
|
||||
const appCredential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
userId: reqBody.userId,
|
||||
appId: appMetadata.slug,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (appCredential) {
|
||||
await prisma.credential.update({
|
||||
where: {
|
||||
id: appCredential.id,
|
||||
},
|
||||
data: {
|
||||
key: keys,
|
||||
},
|
||||
});
|
||||
return res.status(200).json({ message: `Credentials updated for userId: ${reqBody.userId}` });
|
||||
} else {
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
key: keys,
|
||||
userId: reqBody.userId,
|
||||
appId: appMetadata.slug,
|
||||
type: appMetadata.type,
|
||||
},
|
||||
});
|
||||
return res.status(200).json({ message: `Credentials created for userId: ${reqBody.userId}` });
|
||||
}
|
||||
}
|
|
@ -115,40 +115,46 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
|||
const SubTeams = () =>
|
||||
team.children.length ? (
|
||||
<ul className="divide-subtle border-subtle bg-default !static w-full divide-y rounded-md border">
|
||||
{team.children.map((ch, i) => (
|
||||
<li key={i} className="hover:bg-muted w-full">
|
||||
<Link href={`/${ch.slug}`} className="flex items-center justify-between">
|
||||
<div className="flex items-center px-5 py-5">
|
||||
<Avatar
|
||||
size="md"
|
||||
imageSrc={getPlaceholderAvatar(ch?.logo, ch?.name as string)}
|
||||
alt="Team Logo"
|
||||
className="inline-flex justify-center"
|
||||
/>
|
||||
<div className="ms-3 inline-block truncate">
|
||||
<span className="text-default text-sm font-bold">{ch.name}</span>
|
||||
<span className="text-subtle block text-xs">
|
||||
{t("number_member", {
|
||||
count: ch.members.filter((mem) => mem.user.username !== null).length,
|
||||
})}
|
||||
</span>
|
||||
{team.children.map((ch, i) => {
|
||||
const memberCount = team.members.filter(
|
||||
(mem) => mem.subteams?.includes(ch.slug) && mem.accepted
|
||||
).length;
|
||||
return (
|
||||
<li key={i} className="hover:bg-muted w-full">
|
||||
<Link href={`/${ch.slug}`} className="flex items-center justify-between">
|
||||
<div className="flex items-center px-5 py-5">
|
||||
<Avatar
|
||||
size="md"
|
||||
imageSrc={getPlaceholderAvatar(ch?.logo, ch?.name as string)}
|
||||
alt="Team Logo"
|
||||
className="inline-flex justify-center"
|
||||
/>
|
||||
<div className="ms-3 inline-block truncate">
|
||||
<span className="text-default text-sm font-bold">{ch.name}</span>
|
||||
<span className="text-subtle block text-xs">
|
||||
{t("number_member", {
|
||||
count: memberCount,
|
||||
defaultValue: `${memberCount} member${memberCount > 1 ? "s" : ""}`,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AvatarGroup
|
||||
className="mr-6"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
items={ch.members
|
||||
.filter((mem) => mem.user.username !== null)
|
||||
.map(({ user: member }) => ({
|
||||
alt: member.name || "",
|
||||
image: `/${member.username}/avatar.png`,
|
||||
title: member.name || "",
|
||||
}))}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<AvatarGroup
|
||||
className="mr-6"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
items={team.members
|
||||
.filter((mem) => mem.subteams?.includes(ch.slug) && mem.accepted)
|
||||
.map((member) => ({
|
||||
alt: member.name || "",
|
||||
image: `/${member.username}/avatar.png`,
|
||||
title: member.name || "",
|
||||
}))}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="space-y-6" data-testid="event-types">
|
||||
|
@ -251,7 +257,6 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
|||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
||||
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(
|
||||
context.req.headers.host ?? "",
|
||||
|
@ -259,12 +264,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
const team = await getTeamWithMembers({ slug, orgSlug: currentOrgDomain });
|
||||
const ssr = await ssrInit(context, {
|
||||
noI18nPreload: isValidOrgDomain && !team?.parent,
|
||||
});
|
||||
const metadata = teamMetadataSchema.parse(team?.metadata ?? {});
|
||||
console.warn("gSSP, team/[slug] - ", {
|
||||
console.info("gSSP, team/[slug] - ", {
|
||||
isValidOrgDomain,
|
||||
currentOrgDomain,
|
||||
ALLOWED_HOSTNAMES: process.env.ALLOWED_HOSTNAMES,
|
||||
flags: JSON.stringify,
|
||||
flags: JSON.stringify(flags),
|
||||
});
|
||||
// Taking care of sub-teams and orgs
|
||||
if (
|
||||
|
@ -316,7 +324,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
name: member.name,
|
||||
id: member.id,
|
||||
bio: member.bio,
|
||||
subteams: member.subteams,
|
||||
username: member.username,
|
||||
accepted: member.accepted,
|
||||
safeBio: markdownToSafeHTML(member.bio || ""),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -291,9 +291,9 @@
|
|||
"add_another_calendar": "Einen weiteren Kalender hinzufügen",
|
||||
"other": "Sonstige",
|
||||
"email_sign_in_subject": "Ihr Anmelde-Link für {{appName}}",
|
||||
"emailed_you_and_attendees": "Wir haben Ihnen und den anderen Teilnehmern eine Einladung zum Kalender mit allen Details zugeschickt.",
|
||||
"emailed_you_and_attendees_recurring": "Wir haben Ihnen und den anderen Teilnemern eine Kalendereinladung für den ersten Termin dieser wiederkehrenden Termins gesendet.",
|
||||
"emailed_you_and_any_other_attendees": "Wir haben Ihnen und allen anderen Teilnehmern diese Informationen per E-Mail zugeschickt.",
|
||||
"emailed_you_and_attendees": "Wir haben eine E-Mail mit einer Kalendereinladung mit den Details an alle gesendet.",
|
||||
"emailed_you_and_attendees_recurring": "Wir haben eine E-Mail mit einer Kalendereinladung mit den Details für das erste dieser wiederkehrenden Ereignisse an alle gesendet.",
|
||||
"emailed_you_and_any_other_attendees": "Wir haben eine E-Mail mit diesen Informationen an alle gesendet.",
|
||||
"needs_to_be_confirmed_or_rejected": "Ihr Termin muss noch bestätigt oder abgelehnt werden.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Ihr wiederkehrender Termin muss noch bestätigt oder abgelehnt werden.",
|
||||
"user_needs_to_confirm_or_reject_booking": "{{user}} muss den Termin noch bestätigen oder ablehnen.",
|
||||
|
@ -1103,7 +1103,7 @@
|
|||
"reschedule_optional": "Grund für die Verschiebung (optional)",
|
||||
"reschedule_placeholder": "Lassen Sie andere wissen, warum Sie den Termin verschieben müssen",
|
||||
"event_cancelled": "Dieser Termin ist abgesagt",
|
||||
"emailed_information_about_cancelled_event": "Wir haben Ihnen und den anderen Teilnehmern eine E-Mail gesendet, damit alle Bescheid wissen.",
|
||||
"emailed_information_about_cancelled_event": "Wir haben eine E-Mail an alle gesendet, um sie darüber zu informieren.",
|
||||
"this_input_will_shown_booking_this_event": "Diese Eingabe wird bei der Buchung dieses Termins angezeigt",
|
||||
"meeting_url_in_confirmation_email": "Termin-URL ist in der Bestätigungsmail",
|
||||
"url_start_with_https": "URL muss mit http:// oder https:// beginnen",
|
||||
|
|
|
@ -1743,7 +1743,8 @@
|
|||
"show_on_booking_page": "Afficher sur la page de réservation",
|
||||
"get_started_zapier_templates": "Démarrer avec les modèles Zapier",
|
||||
"team_is_unpublished": "{{team}} n'est pas publiée",
|
||||
"team_is_unpublished_description": "Ce lien d'{{entity}} n'est actuellement pas disponible. Veuillez contacter le propriétaire de l'{{entity}} ou lui demander de le publier.",
|
||||
"org_is_unpublished_description": "Ce lien d'organisation n'est actuellement pas disponible. Veuillez contacter le propriétaire de l'organisation ou lui demander de le publier.",
|
||||
"team_is_unpublished_description": "Ce lien d'équipe n'est actuellement pas disponible. Veuillez contacter le propriétaire de l'équipe ou lui demander de le publier.",
|
||||
"team_member": "Membre d'équipe",
|
||||
"a_routing_form": "Un formulaire de routage",
|
||||
"form_description_placeholder": "Description du formulaire",
|
||||
|
@ -2052,7 +2053,7 @@
|
|||
"team_no_event_types": "Cette équipe n'a aucun type d'événement",
|
||||
"seat_options_doesnt_multiple_durations": "L'option par place ne prend pas en charge les durées multiples",
|
||||
"include_calendar_event": "Inclure l'événement du calendrier",
|
||||
"recently_added": "Ajouté récemment",
|
||||
"recently_added": "Ajoutées récemment",
|
||||
"no_members_found": "Aucun membre trouvé",
|
||||
"event_setup_length_error": "Configuration de l'événement : la durée doit être d'au moins 1 minute.",
|
||||
"availability_schedules": "Horaires de disponibilité",
|
||||
|
|
|
@ -1987,7 +1987,7 @@
|
|||
"select_date": "Seleziona la data",
|
||||
"see_all_available_times": "Vedi tutti gli orari disponibili",
|
||||
"org_team_names_example": "ad es., team di marketing",
|
||||
"org_team_names_example_1": "ad es., Team di marketing",
|
||||
"org_team_names_example_1": "ad es., team di marketing",
|
||||
"org_team_names_example_2": "ad es., team vendite",
|
||||
"org_team_names_example_3": "ad es., team di progettazione",
|
||||
"org_team_names_example_4": "ad es., team di ingegneria",
|
||||
|
|
|
@ -17,9 +17,15 @@
|
|||
"verify_email_banner_body": "E-postaların ve takvim bilgilerinin güvenilir bir şekilde teslim edilmesini sağlamak için e-posta adresinizi doğrulayın",
|
||||
"verify_email_email_header": "E-posta adresinizi doğrulayın",
|
||||
"verify_email_email_button": "E-postayı doğrula",
|
||||
"copy_somewhere_safe": "Bu API anahtarını güvenli bir yere kaydedin. API anahtarını tekrar görüntülemeniz mümkün değildir.",
|
||||
"verify_email_email_body": "Lütfen aşağıdaki düğmeyi tıklayarak e-posta adresinizi doğrulayın.",
|
||||
"verify_email_by_code_email_body": "Lütfen aşağıdaki kodu kullanarak e-posta adresinizi doğrulayın.",
|
||||
"verify_email_email_link_text": "Düğmelere tıklamayı sevmiyorsanız işte bağlantı:",
|
||||
"email_verification_code": "Doğrulama kodunu girin",
|
||||
"email_verification_code_placeholder": "Postanıza gönderilen doğrulama kodunu girin",
|
||||
"incorrect_email_verification_code": "Doğrulama kodu yanlış.",
|
||||
"email_sent": "E-posta başarıyla gönderildi",
|
||||
"email_not_sent": "E-posta gönderilirken bir hata oluştu",
|
||||
"event_declined_subject": "Reddedildi: {{title}}, {{date}} tarihinde",
|
||||
"event_cancelled_subject": "İptal edildi: {{date}} tarihindeki {{title}}",
|
||||
"event_request_declined": "Etkinlik talebiniz reddedildi",
|
||||
|
@ -78,6 +84,7 @@
|
|||
"event_awaiting_approval_recurring": "Yinelenen bir etkinlik onayınızı bekliyor",
|
||||
"someone_requested_an_event": "Birisi takviminizde bir etkinlik planlamak istiyor.",
|
||||
"someone_requested_password_reset": "Biri şifrenizi değiştirmek için bir bağlantı talebinde bulundu.",
|
||||
"password_reset_email_sent": "Bu e-posta sistemimizde mevcutsa bir sıfırlama e-postası almanız gerekir.",
|
||||
"password_reset_instructions": "Böyle bir talepte bulunmadıysanız bu e-postayı güvenle yok sayabilirsiniz. Şifreniz değiştirilmeyecektir.",
|
||||
"event_awaiting_approval_subject": "Onay Bekleniyor: {{date}} tarihinde {{title}}",
|
||||
"event_still_awaiting_approval": "Bir etkinlik hâlâ onayınızı bekliyor",
|
||||
|
@ -217,6 +224,10 @@
|
|||
"already_have_an_account": "Zaten bir hesabınız var mı?",
|
||||
"create_account": "Hesap Oluştur",
|
||||
"confirm_password": "Şifreyi onayla",
|
||||
"confirm_auth_change": "Bu işlem, oturum açma yönteminizi değiştirir",
|
||||
"confirm_auth_email_change": "E-posta adresinizi değiştirmek, Cal.com'da oturum açmak için geçerli kimlik doğrulama yönteminizin ilişkisini kesecektir. Sizden yeni e-posta adresinizi onaylamanızı isteyeceğiz. Devam ettiğinizde oturumunuz sonlandırılacak ve e-postanıza gönderilecek talimatlara göre şifrenizi belirledikten sonra mevcut kimlik doğrulama yönteminiz yerine yeni e-posta adresinizi kullanarak giriş yapacaksınız.",
|
||||
"reset_your_password": "E-posta adresinize gönderilen talimatlarla yeni şifrenizi belirleyin.",
|
||||
"email_change": "Yeni e-posta adresiniz ve şifrenizle tekrar giriş yapın.",
|
||||
"create_your_account": "Hesabınızı oluşturun",
|
||||
"sign_up": "Kaydol",
|
||||
"youve_been_logged_out": "Çıkış yaptınız",
|
||||
|
@ -243,6 +254,10 @@
|
|||
"all": "Tümü",
|
||||
"yours": "Hesabınız",
|
||||
"available_apps": "Mevcut Uygulamalar",
|
||||
"available_apps_lower_case": "Mevcut uygulamalar",
|
||||
"available_apps_desc": "Yüklü uygulamanız yok. Aşağıda popüler uygulamaları görüntüleyin ve <1>App Store</1>'da daha fazlasını keşfedin",
|
||||
"fixed_host_helper": "Etkinliğe katılması gereken herkesi ekleyin. <1>Daha fazla bilgi edinin</1>",
|
||||
"round_robin_helper": "Grup üyeleri sırayla katılacak ve etkinlikte yalnızca bir kişi bulunacaktır.",
|
||||
"check_email_reset_password": "E-posta adresinizi kontrol edin. Size şifrenizi sıfırlamanız için bir bağlantı göndereceğiz.",
|
||||
"finish": "Bitir",
|
||||
"organization_general_description": "Ekibinizin dili ve saat dilimi için ayarları yönetin",
|
||||
|
@ -251,6 +266,7 @@
|
|||
"nearly_there_instructions": "Son bir şey; kendiniz hakkında kısa bir açıklama yazmak ve bir resim koymak, rezervasyon almanıza ve insanların gerçekte kiminle rezervasyon yaptıklarını bilmelerine yardımcı olur.",
|
||||
"set_availability_instructions": "Sürekli olarak müsait olduğunuz zaman aralıklarını tanımlayın. Daha sonra başka zaman aralıkları oluşturabilir ve bunları farklı takvimlere atayabilirsiniz.",
|
||||
"set_availability": "Müsaitlik durumunuzu ayarlayın",
|
||||
"availability_settings": "Müsaitlik Ayarları",
|
||||
"continue_without_calendar": "Takvim olmadan devam et",
|
||||
"connect_your_calendar": "Takviminizi bağlayın",
|
||||
"connect_your_video_app": "Video uygulamalarınızı bağlayın",
|
||||
|
@ -392,6 +408,8 @@
|
|||
"allow_dynamic_booking_tooltip": "\"+\" işareti ile birden çok kullanıcı adı eklenerek dinamik olarak oluşturulabilen grup rezervasyon bağlantıları. Örnek: \"{{appName}}/bailey+peer\"",
|
||||
"allow_dynamic_booking": "Katılımcıların dinamik grup rezervasyonları aracılığıyla sizin adınıza rezervasyon yapmalarına izin verin",
|
||||
"dynamic_booking": "Dinamik grup bağlantıları",
|
||||
"allow_seo_indexing": "Arama motorlarının genel içeriğinize erişmesine izin verin",
|
||||
"seo_indexing": "SEO Dizine Eklemeye İzin Verin",
|
||||
"email": "E-posta",
|
||||
"email_placeholder": "derya@example.com",
|
||||
"full_name": "Tam ad",
|
||||
|
@ -406,6 +424,7 @@
|
|||
"booking_requested": "Rezervasyon Talep Edildi",
|
||||
"meeting_ended": "Toplantı Sona Erdi",
|
||||
"form_submitted": "Form Gönderildi",
|
||||
"booking_paid": "Rezervasyon Ücreti Ödendi",
|
||||
"event_triggers": "Etkinlik Tetikleyicileri",
|
||||
"subscriber_url": "Abone URL'si",
|
||||
"create_new_webhook": "Yeni bir web kancası oluştur",
|
||||
|
@ -506,6 +525,7 @@
|
|||
"your_name": "Adınız",
|
||||
"your_full_name": "Tam adınız",
|
||||
"no_name": "Ad yok",
|
||||
"enter_number_between_range": "Lütfen 1 ile {{maxOccurences}} arasında bir sayı girin",
|
||||
"email_address": "E-posta adresi",
|
||||
"enter_valid_email": "Lütfen geçerli bir e-posta adresi girin",
|
||||
"location": "Konum",
|
||||
|
@ -543,6 +563,7 @@
|
|||
"leave": "Ayrıl",
|
||||
"profile": "Profil",
|
||||
"my_team_url": "Ekibimin URL'si",
|
||||
"my_teams": "Ekiplerim",
|
||||
"team_name": "Ekip Adı",
|
||||
"your_team_name": "Ekibinizin adı",
|
||||
"team_updated_successfully": "Ekip başarıyla güncellendi",
|
||||
|
@ -570,6 +591,8 @@
|
|||
"invite_new_member": "Yeni bir ekip üyesi davet et",
|
||||
"invite_new_member_description": "Not: Bu, aboneliğiniz için <1>ekstra yer ücretine (15 $/ay)</1> mal olacak.",
|
||||
"invite_new_team_member": "Ekibinize birini davet edin.",
|
||||
"upload_csv_file": "Bir .csv dosyası yükleyin",
|
||||
"invite_via_email": "E-postayla davet et",
|
||||
"change_member_role": "Ekip üyesi rolünü değiştir",
|
||||
"disable_cal_branding": "{{appName}} markasını devre dışı bırak",
|
||||
"disable_cal_branding_description": "Herkese açık sayfalarınızdan tüm {{appName}} markalarını gizleyin.",
|
||||
|
@ -842,6 +865,7 @@
|
|||
"team_view_user_availability_disabled": "Müsaitlik durumunu görüntülemek için kullanıcının daveti kabul etmesi gerekiyor",
|
||||
"set_as_away": "Kendinizi dışarıda olarak ayarlayın",
|
||||
"set_as_free": "Dışarıda durumunu devre dışı bırak",
|
||||
"toggle_away_error": "Uzakta durumu güncellenirken hata oluştu",
|
||||
"user_away": "Bu kullanıcı şu anda uzakta.",
|
||||
"user_away_description": "Rezervasyon yapmaya çalıştığınız kişi kendisini \"Uzakta\" olarak ayarladığı için şu anda yeni rezervasyon kabul etmiyor.",
|
||||
"meet_people_with_the_same_tokens": "Aynı token'lara sahip insanlarla tanışın",
|
||||
|
@ -851,6 +875,7 @@
|
|||
"account_managed_by_identity_provider_description": "E-posta adresinizi ve şifrenizi değiştirmek, iki adımlı kimlik doğrulamayı etkinleştirmek ve daha fazlası için lütfen {{provider}} hesap ayarlarınızı ziyaret edin.",
|
||||
"signin_with_google": "Google ile giriş yap",
|
||||
"signin_with_saml": "SAML ile giriş yap",
|
||||
"signin_with_saml_oidc": "SAML/OIDC ile giriş yapın",
|
||||
"you_will_need_to_generate": "Entegrasyon sayfasında bir erişim token'ı oluşturmanız gerekir.",
|
||||
"import": "İçe aktar",
|
||||
"import_from": "Şuradan içe aktar",
|
||||
|
@ -1038,7 +1063,9 @@
|
|||
"your_unique_api_key": "Benzersiz API anahtarınız",
|
||||
"copy_safe_api_key": "Bu API anahtarını kopyalayın ve güvenli bir yere kaydedin. Bu anahtarı kaybederseniz yeni bir anahtar oluşturmanız gerekir.",
|
||||
"zapier_setup_instructions": "<0>Zapier hesabınıza giriş yapın ve yeni bir Zap oluşturun.</0><1>Trigger uygulamanız olarak Cal.com'u seçin. Ayrıca bir Trigger etkinliği seçin.</1><2>Hesabınızı seçin ve ardından Benzersiz API Anahtarınızı girin.</2><3>Trigger'ınızı test edin.</3><4>Hazırsınız!</4 >",
|
||||
"make_setup_instructions": "<0>Make Davet Bağlantısına</0></1> <0>gidin<1> ve Cal.com uygulamasını yükleyin.<0><1>Make hesabınıza giriş yapın ve yeni bir Senaryo oluşturun.</1><2>Tetikleyici uygulamanız olarak Cal.com'u seçin. Ayrıca bir Tetikleyici etkinliği seçin.</2><3>Hesabınızı seçin ve ardından Benzersiz API Anahtarınızı girin.</3><4>Tetikleyicinizi test edin.</4><5>Hazırsınız!</5>",
|
||||
"install_zapier_app": "Lütfen öncelikle App Store'dan Zapier uygulamasını yükleyin.",
|
||||
"install_make_app": "Lütfen öncelikle App Store'dan Make uygulamasını yükleyin.",
|
||||
"connect_apple_server": "Apple Sunucusuna Bağlanın",
|
||||
"calendar_url": "Takvim URL'si",
|
||||
"apple_server_generate_password": "Şurada {{appName}} ile kullanmak üzere uygulamaya özel bir şifre oluşturun:",
|
||||
|
@ -1248,6 +1275,7 @@
|
|||
"error_updating_settings": "Ayarlar güncellenirken bir hata oluştu",
|
||||
"personal_cal_url": "Kişisel {{appName}} URL'm",
|
||||
"bio_hint": "Birkaç cümleyle kendinizden bahsedin. Bu, kişisel URL sayfanızda görünecektir.",
|
||||
"user_has_no_bio": "Bu kullanıcı henüz bir biyografi eklemedi.",
|
||||
"delete_account_modal_title": "Hesabı Sil",
|
||||
"confirm_delete_account_modal": "{{appName}} hesabınızı silmek istediğinizden emin misiniz?",
|
||||
"delete_my_account": "Hesabımı sil",
|
||||
|
@ -1430,6 +1458,7 @@
|
|||
"add_limit": "Kısıtlama Ekleyin",
|
||||
"team_name_required": "Ekip adı gereklidir",
|
||||
"show_attendees": "Katılımcı bilgilerini misafirler arasında paylaşın",
|
||||
"show_available_seats_count": "Mevcut koltuk sayısını göster",
|
||||
"how_booking_questions_as_variables": "Rezervasyon soruları değişken olarak nasıl kullanılır?",
|
||||
"format": "Biçim",
|
||||
"uppercase_for_letters": "Tüm harfleri büyük harf olarak kullanın",
|
||||
|
@ -1663,9 +1692,11 @@
|
|||
"delete_sso_configuration_confirmation_description": "{{connectionType}} yapılandırmasını silmek istediğinizden emin misiniz? {{connectionType}} girişini kullanan ekip üyeleriniz artık Cal.com'a erişemeyecek.",
|
||||
"organizer_timezone": "Organizatörün saat dilimi",
|
||||
"email_user_cta": "Daveti görüntüle",
|
||||
"email_no_user_invite_heading_team": "{{appName}} ekibine katılmaya davet edildiniz",
|
||||
"email_no_user_invite_heading_org": "{{appName}} grubuna katılmaya davet edildiniz",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}}, sizi {{appName}} ekibine katılmaya davet etti. {{appName}}, size ve ekibinize e-posta iletişimine ihtiyaç duymadan toplantı planlama yapma olanağı sağlayan bir etkinlik planlayıcıdır.",
|
||||
"email_user_invite_subheading_team": "{{invitedBy}}, sizi {{appName}} uygulamasındaki `{{teamName}}` ekibine katılmaya davet etti. {{appName}}, size ve ekibinize e-posta iletişimine ihtiyaç duymadan toplantı planlama yapma olanağı sağlayan bir etkinlik planlayıcıdır.",
|
||||
"email_user_invite_subheading_org": "{{invitedBy}}, sizi {{appName}} uygulamasındaki `{{teamName}}` grubuna katılmaya davet etti. {{appName}}, size ve grubunuza e-posta iletişimine ihtiyaç duymadan toplantı planlama yapma olanağı sağlayan bir etkinlik planlayıcıdır.",
|
||||
"email_no_user_invite_steps_intro": "{{entity}} ekibinizle birlikte kısa sürede ve sorunsuz planlama yapmanın keyfini çıkarmanız için birkaç adımda size rehberlik edeceğiz.",
|
||||
"email_no_user_step_one": "Kullanıcı adınızı seçin",
|
||||
"email_no_user_step_two": "Takvim hesabınızı bağlayın",
|
||||
|
@ -1836,6 +1867,8 @@
|
|||
"insights_no_data_found_for_filter": "Seçili filtre veya tarihler için veri bulunamadı.",
|
||||
"acknowledge_booking_no_show_fee": "Bu etkinliğe katılmadığım takdirde kartımdan {{amount, currency}} tutarında katılmama ücreti alınmasını kabul ediyorum.",
|
||||
"card_details": "Kart bilgileri",
|
||||
"something_went_wrong_on_our_end": "Bizden kaynaklanan bir sorun oluştu. Sorunu çözebilmemiz için lütfen destek ekibimizle iletişime geçin.",
|
||||
"please_provide_following_text_to_suppport": "Size daha iyi yardımcı olabilmemiz için lütfen destek ekibiyle iletişime geçtiğinizde aşağıdaki bilgileri sağlayın",
|
||||
"seats_and_no_show_fee_error": "Şu anda koltuklar etkinleştirilemiyor ve katılmama ücreti tahsil edilemiyor",
|
||||
"complete_your_booking": "Rezervasyonunuzu tamamlayın",
|
||||
"complete_your_booking_subject": "Rezervasyonunuzu tamamlayın: {{title}}, {{date}}",
|
||||
|
@ -1869,6 +1902,9 @@
|
|||
"open_dialog_with_element_click": "Biri bir ögeye tıkladığında Cal iletişim kutunuzu açın.",
|
||||
"need_help_embedding": "Yardıma mı ihtiyacınız var? Cal'i Wix, Squarespace veya WordPress'e yerleştirmek için kılavuzlarımıza göz atın. Ayrıca Sıkça Sorulan Sorular bölümümüze göz atın veya gelişmiş yerleştirme seçeneklerini keşfedin.",
|
||||
"book_my_cal": "Cal hesabımda rezerve et",
|
||||
"first_name": "Ad",
|
||||
"last_name": "Soyad",
|
||||
"first_last_name": "Ad, Soyad",
|
||||
"invite_as": "Şu şekilde davet et:",
|
||||
"form_updated_successfully": "Form başarıyla güncellendi.",
|
||||
"disable_attendees_confirmation_emails": "Katılımcılar için varsayılan onay e-postalarını devre dışı bırakın",
|
||||
|
@ -1882,6 +1918,7 @@
|
|||
"first_event_type_webhook_description": "Bu etkinlik türü için ilk web kancanızı oluşturun",
|
||||
"install_app_on": "Uygulamayı şuraya yükle:",
|
||||
"create_for": "Oluşturun",
|
||||
"currency": "Para birimi",
|
||||
"organization_banner_description": "Ekiplerinizin, döngüsel ve toplu planlama ile paylaşılan uygulamalar, iş akışları ve olay türleri oluşturabileceği bir ortam oluşturun.",
|
||||
"organization_banner_title": "Kuruluşları birden çok ekiple yönetin",
|
||||
"set_up_your_organization": "Kuruluşunuzu düzenleyin",
|
||||
|
@ -1942,9 +1979,79 @@
|
|||
"insights_team_filter": "Ekip: {{teamName}}",
|
||||
"insights_user_filter": "Kullanıcı: {{userName}}",
|
||||
"insights_subtitle": "Etkinliklerinizdeki rezervasyon insights'ı görüntüleyin",
|
||||
"location_options": "{{locationCount}} konum seçenekleri",
|
||||
"custom_plan": "Özel Plan",
|
||||
"email_embed": "E-posta Yerleştirme",
|
||||
"add_times_to_your_email": "Uygun zamanlardan bazılarını seçin ve bunları E-postanıza ekleyin",
|
||||
"select_time": "Saati Seçin",
|
||||
"select_date": "Tarihi Seçin",
|
||||
"see_all_available_times": "Tüm müsait saatleri görün",
|
||||
"org_team_names_example": "Örneğin. Pazarlama ekibi",
|
||||
"org_team_names_example_1": "Örneğin. Pazarlama ekibi",
|
||||
"org_team_names_example_2": "Örn. Satış Ekibi",
|
||||
"org_team_names_example_3": "Örn. Tasarım Ekibi",
|
||||
"org_team_names_example_4": "Örn. Mühendislik Ekibi",
|
||||
"org_team_names_example_5": "Örn. Veri Analiz Ekibi",
|
||||
"org_max_team_warnings": "Daha sonra daha fazla ekip ekleyebilirsiniz.",
|
||||
"what_is_this_meeting_about": "Bu toplantının konusu nedir?",
|
||||
"add_to_team": "Ekibe ekle",
|
||||
"remove_users_from_org": "Kullanıcıları gruptan kaldır",
|
||||
"remove_users_from_org_confirm": "{{userCount}} kullanıcıyı bu gruptan kaldırmak istediğinizden emin misiniz?",
|
||||
"user_has_no_schedules": "Bu kullanıcı henüz herhangi bir plan ayarlamadı",
|
||||
"user_isnt_in_any_teams": "Bu kullanıcı şu anda hiçbir ekipte değil",
|
||||
"requires_booker_email_verification": "Rezervasyonu yapan kişinin e-posta doğrulaması gereklidir",
|
||||
"description_requires_booker_email_verification": "Etkinlikleri planlamadan önce rezervasyon yapan kişinin e-posta doğrulaması yapmasını sağlayın",
|
||||
"requires_confirmation_mandatory": "Metin mesajları yalnızca katılımcılardan etkinlik türü onayı alınması gerektiğinde gönderilebilir.",
|
||||
"kyc_verification_information": "Güvenliği sağlamak için katılımcılara kısa mesaj göndermeden önce {{teamOrAccount}} hesabınızı doğrulamanız gereklidir. Lütfen <a>{{supportEmail}}</a> adresinden bizimle iletişime geçin ve aşağıdaki bilgileri sağlayın:",
|
||||
"kyc_verification_documents": "<ul><li>{{teamOrUser}} kullanıcınız</li><li>Kuruluşlar için: Doğrulama Belgelerinizi ekleyin</li><li>Bireyler için: Devlet tarafından verilmiş bir kimlik belgesini ekleyin</li></ul>",
|
||||
"verify_team_or_account": "{{teamOrAccount}} hesabını doğrulayın",
|
||||
"verify_account": "Hesabı Doğrula",
|
||||
"kyc_verification": "KYC Doğrulaması",
|
||||
"organizations": "Gruplar",
|
||||
"org_admin_other_teams": "Diğer ekipler",
|
||||
"org_admin_other_teams_description": "Burada, kuruluşunuzda katılmadığınız ekipleri görebilirsiniz. Gerekirse onlara katılabilirsiniz.",
|
||||
"no_other_teams_found": "Başka ekip bulunamadı",
|
||||
"no_other_teams_found_description": "Bu kuruluşta başka ekip yok.",
|
||||
"attendee_first_name_variable": "Katılımcının adı",
|
||||
"attendee_last_name_variable": "Katılımcının soyadı",
|
||||
"attendee_first_name_info": "Rezervasyon yaptıran kişinin adı",
|
||||
"attendee_last_name_info": "Rezervasyon yaptıran kişinin soyadı",
|
||||
"me": "Ben",
|
||||
"verify_team_tooltip": "Katılımcılara mesaj göndermeyi etkinleştirmek için ekibinizi doğrulayın",
|
||||
"member_removed": "Üye kaldırıldı",
|
||||
"my_availability": "Müsaitlik Durumum",
|
||||
"team_availability": "Ekibin Müsaitlik Durumu",
|
||||
"backup_code": "Yedekleme Kodu",
|
||||
"backup_codes": "Yedekleme Kodları",
|
||||
"backup_code_instructions": "Her yedekleme kodu, kimlik doğrulayıcınız olmadan erişim izni vermek için tam olarak bir kez kullanılabilir.",
|
||||
"backup_codes_copied": "Yedekleme kodları kopyalandı!",
|
||||
"incorrect_backup_code": "Yedekleme kodu yanlış.",
|
||||
"lost_access": "Erişim kaybedildi",
|
||||
"missing_backup_codes": "Yedekleme kodu bulunamadı. Lütfen ayarlarınızdan yedekleme kodu oluşturun.",
|
||||
"admin_org_notification_email_subject": "Yeni kuruluş oluşturuldu: eylem bekleniyor",
|
||||
"hi_admin": "Merhaba Yönetici",
|
||||
"admin_org_notification_email_title": "Kuruluş, DNS kurulumu gerektiriyor",
|
||||
"admin_org_notification_email_body_part1": "\"{{orgSlug}}\" kısa adres adıyla bir kuruluş oluşturuldu.<br /><br />Lütfen DNS kaydınızı yeni kuruluşa karşılık gelen alt alan adını ana uygulamanın çalıştığı yere yönlendirecek şekilde yapılandırdığınızdan emin olun. Aksi halde kuruluş çalışmayacaktır.<br /><br />Bir alt alan adını, kuruluş profil sayfasını yükleyecek şekilde uygulamalarına işaret edecek şekilde yapılandırmaya yönelik en temel seçenekleri burada bulabilirsiniz.<br />< br />Bunu A Record ile de yapabilirsiniz:",
|
||||
"admin_org_notification_email_body_part2": "Veya CNAME kaydı:",
|
||||
"admin_org_notification_email_body_part3": "Alt alan adını yapılandırdıktan sonra lütfen DNS yapılandırmasını Kuruluş Yönetici Ayarlarında olduğu gibi işaretleyin.",
|
||||
"admin_org_notification_email_cta": "Kuruluş Yönetici Ayarlarına gidin",
|
||||
"org_has_been_processed": "Kuruluş işlendi",
|
||||
"org_error_processing": "Bu kuruluş işlenirken bir hata oluştu",
|
||||
"orgs_page_description": "Tüm kuruluşların listesi. Bir kuruluşun kabul edilmesi, söz konusu e-posta alan adına sahip tüm kullanıcıların e-posta doğrulaması OLMADAN kaydolmasına imkan tanır.",
|
||||
"unverified": "Doğrulanmadı",
|
||||
"dns_missing": "DNS Eksik",
|
||||
"mark_dns_configured": "DNS yapılandırılmış olarak işaretle",
|
||||
"value": "Değer",
|
||||
"your_organization_updated_sucessfully": "Kuruluşunuz başarıyla güncellendi",
|
||||
"team_no_event_types": "Bu ekipte etkinlik türü yok",
|
||||
"seat_options_doesnt_multiple_durations": "Koltuk seçeneği birden fazla süreyi desteklemiyor",
|
||||
"include_calendar_event": "Takvim etkinliğini dahil et",
|
||||
"recently_added": "Son eklenen",
|
||||
"no_members_found": "Üye bulunamadı",
|
||||
"event_setup_length_error": "Etkinlik Kurulumu: Süre en az 1 dakika olmalıdır.",
|
||||
"availability_schedules": "Müsaitlik Planları",
|
||||
"view_only_edit_availability_not_onboarded": "Bu kullanıcı katılımı tamamlamadı. Katılımı tamamlayana kadar uygunluk durumlarını ayarlayamazsınız.",
|
||||
"view_only_edit_availability": "Bu kullanıcının müsaitlik durumunu görüntülüyorsunuz. Yalnızca kendi uygunluk durumunuzu düzenleyebilirsiniz.",
|
||||
"edit_users_availability": "Kullanıcının müsaitlik durumunu düzenleyin: {{username}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { appRouter } from "@calcom/trpc/server/routers/_app";
|
|||
* Automatically prefetches i18n based on the passed in `context`-object to prevent i18n-flickering.
|
||||
* Make sure to `return { props: { trpcState: ssr.dehydrate() } }` at the end.
|
||||
*/
|
||||
export async function ssrInit(context: GetServerSidePropsContext) {
|
||||
export async function ssrInit(context: GetServerSidePropsContext, options?: { noI18nPreload: boolean }) {
|
||||
const ctx = await createContext(context);
|
||||
const locale = await getLocaleFromRequest(context.req);
|
||||
const i18n = await serverSideTranslations(locale, ["common", "vital"]);
|
||||
|
@ -27,7 +27,9 @@ export async function ssrInit(context: GetServerSidePropsContext) {
|
|||
|
||||
await Promise.allSettled([
|
||||
// always preload "viewer.public.i18n"
|
||||
ssr.viewer.public.i18n.prefetch({ locale, CalComVersion: CALCOM_VERSION }),
|
||||
!options?.noI18nPreload
|
||||
? ssr.viewer.public.i18n.prefetch({ locale, CalComVersion: CALCOM_VERSION })
|
||||
: Promise.resolve({}),
|
||||
// So feature flags are available on first render
|
||||
ssr.viewer.features.map.prefetch(),
|
||||
// Provides a better UX to the users who have already upgraded.
|
||||
|
|
|
@ -3,8 +3,8 @@ import type { NextApiRequest } from "next";
|
|||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { decodeOAuthState } from "./decodeOAuthState";
|
||||
import { throwIfNotHaveAdminAccessToTeam } from "./throwIfNotHaveAdminAccessToTeam";
|
||||
import { decodeOAuthState } from "../oauth/decodeOAuthState";
|
||||
import { throwIfNotHaveAdminAccessToTeam } from "../throwIfNotHaveAdminAccessToTeam";
|
||||
|
||||
/**
|
||||
* This function is used to create app credentials for either a user or a team
|
|
@ -1,6 +1,6 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import type { IntegrationOAuthCallbackState } from "../types";
|
||||
import type { IntegrationOAuthCallbackState } from "../../types";
|
||||
|
||||
export function decodeOAuthState(req: NextApiRequest) {
|
||||
if (typeof req.query.state !== "string") {
|
|
@ -1,6 +1,6 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import type { IntegrationOAuthCallbackState } from "../types";
|
||||
import type { IntegrationOAuthCallbackState } from "../../types";
|
||||
|
||||
export function encodeOAuthState(req: NextApiRequest) {
|
||||
if (typeof req.query.state !== "string") {
|
|
@ -0,0 +1,36 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
|
||||
const minimumTokenResponseSchema = z.object({
|
||||
access_token: z.string(),
|
||||
// Assume that any property with a number is the expiry
|
||||
[z.string().toString()]: z.number(),
|
||||
// Allow other properties in the token response
|
||||
[z.string().optional().toString()]: z.unknown().optional(),
|
||||
});
|
||||
|
||||
export type ParseRefreshTokenResponse<S extends z.ZodTypeAny> =
|
||||
| z.infer<S>
|
||||
| z.infer<typeof minimumTokenResponseSchema>;
|
||||
|
||||
const parseRefreshTokenResponse = (response: any, schema: z.ZodTypeAny) => {
|
||||
let refreshTokenResponse;
|
||||
if (APP_CREDENTIAL_SHARING_ENABLED && process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT) {
|
||||
refreshTokenResponse = minimumTokenResponseSchema.safeParse(response);
|
||||
} else {
|
||||
refreshTokenResponse = schema.safeParse(response);
|
||||
}
|
||||
|
||||
if (!refreshTokenResponse.success) {
|
||||
throw new Error("Invalid refreshed tokens were returned");
|
||||
}
|
||||
|
||||
if (!refreshTokenResponse.data.refresh_token) {
|
||||
refreshTokenResponse.data.refresh_token = "refresh_token";
|
||||
}
|
||||
|
||||
return refreshTokenResponse.data;
|
||||
};
|
||||
|
||||
export default parseRefreshTokenResponse;
|
|
@ -0,0 +1,22 @@
|
|||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
|
||||
const refreshOAuthTokens = async (refreshFunction: () => any, appSlug: string, userId: number | null) => {
|
||||
// Check that app syncing is enabled and that the credential belongs to a user
|
||||
if (APP_CREDENTIAL_SHARING_ENABLED && process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT && userId) {
|
||||
// Customize the payload based on what your endpoint requires
|
||||
// The response should only contain the access token and expiry date
|
||||
const response = await fetch(process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT, {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
calcomUserId: userId.toString(),
|
||||
appSlug,
|
||||
}),
|
||||
});
|
||||
return response;
|
||||
} else {
|
||||
const response = await refreshFunction();
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
export default refreshOAuthTokens;
|
|
@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
||||
const scopes = [
|
||||
"https://www.googleapis.com/auth/calendar.readonly",
|
||||
|
|
|
@ -5,9 +5,9 @@ import { WEBAPP_URL_FOR_OAUTH, CAL_URL } from "@calcom/lib/constants";
|
|||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
|
|
@ -18,6 +18,9 @@ import type {
|
|||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
import { getGoogleAppKeys } from "./getGoogleAppKeys";
|
||||
import { googleCredentialSchema } from "./googleCredentialSchema";
|
||||
|
||||
|
@ -81,14 +84,24 @@ export default class GoogleCalendarService implements Calendar {
|
|||
|
||||
const refreshAccessToken = async (myGoogleAuth: Awaited<ReturnType<typeof getGoogleAuth>>) => {
|
||||
try {
|
||||
const { res } = await myGoogleAuth.refreshToken(googleCredentials.refresh_token);
|
||||
const res = await refreshOAuthTokens(
|
||||
async () => {
|
||||
const fetchTokens = await myGoogleAuth.refreshToken(googleCredentials.refresh_token);
|
||||
return fetchTokens.res;
|
||||
},
|
||||
"google-calendar",
|
||||
credential.userId
|
||||
);
|
||||
const token = res?.data;
|
||||
googleCredentials.access_token = token.access_token;
|
||||
googleCredentials.expiry_date = token.expiry_date;
|
||||
const key = googleCredentialSchema.parse(googleCredentials);
|
||||
const parsedKey: ParseRefreshTokenResponse<typeof googleCredentialSchema> = parseRefreshTokenResponse(
|
||||
googleCredentials,
|
||||
googleCredentialSchema
|
||||
);
|
||||
await prisma.credential.update({
|
||||
where: { id: credential.id },
|
||||
data: { key },
|
||||
data: { key: { ...parsedKey } as Prisma.InputJsonValue },
|
||||
});
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
} catch (err) {
|
||||
|
|
|
@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
||||
const scopes = ["crm.objects.contacts.read", "crm.objects.contacts.write"];
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
|
|
@ -23,6 +23,7 @@ import type {
|
|||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
import type { HubspotToken } from "../api/callback";
|
||||
|
||||
const hubspotClient = new hubspot.Client();
|
||||
|
@ -173,13 +174,18 @@ export default class HubspotCalendarService implements Calendar {
|
|||
|
||||
const refreshAccessToken = async (refreshToken: string) => {
|
||||
try {
|
||||
const hubspotRefreshToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
|
||||
"refresh_token",
|
||||
undefined,
|
||||
WEBAPP_URL + "/api/integrations/hubspot/callback",
|
||||
this.client_id,
|
||||
this.client_secret,
|
||||
refreshToken
|
||||
const hubspotRefreshToken: HubspotToken = await refreshOAuthTokens(
|
||||
async () =>
|
||||
await hubspotClient.oauth.tokensApi.createToken(
|
||||
"refresh_token",
|
||||
undefined,
|
||||
WEBAPP_URL + "/api/integrations/hubspot/callback",
|
||||
this.client_id,
|
||||
this.client_secret,
|
||||
refreshToken
|
||||
),
|
||||
"hubspot",
|
||||
credential.userId
|
||||
);
|
||||
|
||||
// set expiry date as offset from current time.
|
||||
|
|
|
@ -5,8 +5,8 @@ import { z } from "zod";
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
import { LARK_HOST } from "../common";
|
||||
|
||||
const larkKeysSchema = z.object({
|
||||
|
|
|
@ -6,8 +6,8 @@ import logger from "@calcom/lib/logger";
|
|||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import { LARK_HOST } from "../common";
|
||||
import { getAppAccessToken } from "../lib/AppAccessToken";
|
||||
import type { LarkAuthCredentials } from "../types/LarkCalendar";
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
import { handleLarkError, isExpired, LARK_HOST } from "../common";
|
||||
import type {
|
||||
CreateAttendeesResp,
|
||||
|
@ -63,17 +64,22 @@ export default class LarkCalendarService implements Calendar {
|
|||
}
|
||||
try {
|
||||
const appAccessToken = await getAppAccessToken();
|
||||
const resp = await fetch(`${this.url}/authen/v1/refresh_access_token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${appAccessToken}`,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
const resp = await refreshOAuthTokens(
|
||||
async () =>
|
||||
await fetch(`${this.url}/authen/v1/refresh_access_token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${appAccessToken}`,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
}),
|
||||
"lark-calendar",
|
||||
credential.userId
|
||||
);
|
||||
|
||||
const data = await handleLarkError<RefreshTokenResp>(resp, this.log);
|
||||
this.log.debug(
|
||||
|
|
|
@ -3,8 +3,8 @@ import { stringify } from "querystring";
|
|||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
||||
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
|||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
|
||||
const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];
|
||||
|
||||
|
|
|
@ -17,6 +17,9 @@ import type {
|
|||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
import type { O365AuthCredentials } from "../types/Office365Calendar";
|
||||
import { getOfficeAppKeys } from "./getOfficeAppKeys";
|
||||
|
||||
|
@ -241,28 +244,26 @@ export default class Office365CalendarService implements Calendar {
|
|||
|
||||
const refreshAccessToken = async (o365AuthCredentials: O365AuthCredentials) => {
|
||||
const { client_id, client_secret } = await getOfficeAppKeys();
|
||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||
client_id,
|
||||
refresh_token: o365AuthCredentials.refresh_token,
|
||||
grant_type: "refresh_token",
|
||||
client_secret,
|
||||
}),
|
||||
});
|
||||
const response = await refreshOAuthTokens(
|
||||
async () =>
|
||||
await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||
client_id,
|
||||
refresh_token: o365AuthCredentials.refresh_token,
|
||||
grant_type: "refresh_token",
|
||||
client_secret,
|
||||
}),
|
||||
}),
|
||||
"office365-calendar",
|
||||
credential.userId
|
||||
);
|
||||
const responseJson = await handleErrorsJson(response);
|
||||
const tokenResponse = refreshTokenResponseSchema.safeParse(responseJson);
|
||||
o365AuthCredentials = { ...o365AuthCredentials, ...(tokenResponse.success && tokenResponse.data) };
|
||||
if (!tokenResponse.success) {
|
||||
console.error(
|
||||
"Outlook error grabbing new tokens ~ zodError:",
|
||||
tokenResponse.error,
|
||||
"MS response:",
|
||||
responseJson
|
||||
);
|
||||
}
|
||||
const tokenResponse: ParseRefreshTokenResponse<typeof refreshTokenResponseSchema> =
|
||||
parseRefreshTokenResponse(responseJson, refreshTokenResponseSchema);
|
||||
o365AuthCredentials = { ...o365AuthCredentials, ...tokenResponse };
|
||||
await prisma.credential.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
|
|
|
@ -3,8 +3,8 @@ import { stringify } from "querystring";
|
|||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
||||
const scopes = ["OnlineMeetings.ReadWrite", "offline_access"];
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
|||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
|
||||
const scopes = ["OnlineMeetings.ReadWrite", "offline_access"];
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { PartialReference } from "@calcom/types/EventManager";
|
|||
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
@ -57,16 +58,21 @@ const o365Auth = async (credential: CredentialPayload) => {
|
|||
const o365AuthCredentials = credential.key as unknown as O365AuthCredentials;
|
||||
|
||||
const refreshAccessToken = async (refreshToken: string) => {
|
||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
client_secret,
|
||||
}),
|
||||
});
|
||||
const response = await refreshOAuthTokens(
|
||||
async () =>
|
||||
await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
client_secret,
|
||||
}),
|
||||
}),
|
||||
"msteams",
|
||||
credential.userId
|
||||
);
|
||||
|
||||
const responseBody = await handleErrorsJson<ITokenResponse>(response);
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
||||
let consumer_key = "";
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
|
||||
let consumer_key = "";
|
||||
let consumer_secret = "";
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { TokenResponse } from "jsforce";
|
||||
import jsforce from "jsforce";
|
||||
import { RRule } from "rrule";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getLocation } from "@calcom/lib/CalEventParser";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
@ -16,6 +17,8 @@ import type {
|
|||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
|
||||
type ExtendedTokenResponse = TokenResponse & {
|
||||
instance_url: string;
|
||||
|
@ -34,6 +37,16 @@ const sfApiErrors = {
|
|||
INVALID_EVENTWHOIDS: "INVALID_FIELD: No such column 'EventWhoIds' on sobject of type Event",
|
||||
};
|
||||
|
||||
const salesforceTokenSchema = z.object({
|
||||
id: z.string(),
|
||||
issued_at: z.string(),
|
||||
instance_url: z.string(),
|
||||
signature: z.string(),
|
||||
access_token: z.string(),
|
||||
scope: z.string(),
|
||||
token_type: z.string(),
|
||||
});
|
||||
|
||||
export default class SalesforceCalendarService implements Calendar {
|
||||
private integrationName = "";
|
||||
private conn: Promise<jsforce.Connection>;
|
||||
|
@ -60,6 +73,29 @@ export default class SalesforceCalendarService implements Calendar {
|
|||
|
||||
const credentialKey = credential.key as unknown as ExtendedTokenResponse;
|
||||
|
||||
const response = await fetch("https://login.salesforce.com/services/oauth2/token", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
client_id: consumer_key,
|
||||
client_secret: consumer_secret,
|
||||
refresh_token: credentialKey.refresh_token,
|
||||
format: "json",
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.statusText !== "OK") throw new HttpError({ statusCode: 400, message: response.statusText });
|
||||
|
||||
const accessTokenJson = await response.json();
|
||||
|
||||
const accessTokenParsed: ParseRefreshTokenResponse<typeof salesforceTokenSchema> =
|
||||
parseRefreshTokenResponse(accessTokenJson, salesforceTokenSchema);
|
||||
|
||||
await prisma.credential.update({
|
||||
where: { id: credential.id },
|
||||
data: { key: { ...accessTokenParsed, refresh_token: credentialKey.refresh_token } },
|
||||
});
|
||||
|
||||
return new jsforce.Connection({
|
||||
clientId: consumer_key,
|
||||
clientSecret: consumer_secret,
|
||||
|
|
|
@ -2,8 +2,8 @@ import type { Prisma } from "@prisma/client";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import type { StripeData } from "../lib/server";
|
||||
import stripe from "../lib/server";
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
|
|
@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import config from "../config.json";
|
||||
import { getWebexAppKeys } from "../lib/getWebexAppKeys";
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import type { CredentialPayload } from "@calcom/types/Credential";
|
|||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
import { getWebexAppKeys } from "./getWebexAppKeys";
|
||||
|
||||
/** @link https://developer.webex.com/docs/meetings **/
|
||||
|
@ -58,18 +59,23 @@ const webexAuth = (credential: CredentialPayload) => {
|
|||
const refreshAccessToken = async (refreshToken: string) => {
|
||||
const { client_id, client_secret } = await getWebexAppKeys();
|
||||
|
||||
const response = await fetch("https://webexapis.com/v1/access_token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
client_id: client_id,
|
||||
client_secret: client_secret,
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
const response = await refreshOAuthTokens(
|
||||
async () =>
|
||||
await fetch("https://webexapis.com/v1/access_token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
client_id: client_id,
|
||||
client_secret: client_secret,
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
}),
|
||||
"webex",
|
||||
credential.userId
|
||||
);
|
||||
|
||||
const responseBody = await handleWebexResponse(response, credential.id);
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const clientId = typeof appKeys.client_id === "string" ? appKeys.client_id : "";
|
||||
if (!clientId) return res.status(400).json({ message: "Zoho Bigin client_id missing." });
|
||||
|
||||
const redirectUri = WEBAPP_URL + `/api/integrations/${appConfig.slug}/callback`;
|
||||
const redirectUri = WEBAPP_URL + `/api/integrations/zoho-bigin/callback`;
|
||||
|
||||
const authUrl = axios.getUri({
|
||||
url: "https://accounts.zoho.com/oauth/v2/auth",
|
||||
|
|
|
@ -5,10 +5,10 @@ import qs from "qs";
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
import { appKeysSchema } from "../zod";
|
||||
|
||||
export type BiginToken = {
|
||||
|
@ -81,11 +82,16 @@ export default class BiginCalendarService implements Calendar {
|
|||
refresh_token: credentialKey.refresh_token,
|
||||
};
|
||||
|
||||
const tokenInfo = await axios.post(accountsUrl, qs.stringify(formData), {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
},
|
||||
});
|
||||
const tokenInfo = await refreshOAuthTokens(
|
||||
async () =>
|
||||
await axios.post(accountsUrl, qs.stringify(formData), {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
},
|
||||
}),
|
||||
"zoho-bigin",
|
||||
credentialId
|
||||
);
|
||||
|
||||
if (!tokenInfo.data.error) {
|
||||
// set expiry date as offset from current time.
|
||||
|
|
|
@ -3,8 +3,8 @@ import { stringify } from "querystring";
|
|||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
||||
let client_id = "";
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ import qs from "qs";
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
|||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
|
||||
export type ZohoToken = {
|
||||
scope: string;
|
||||
|
@ -200,14 +201,19 @@ export default class ZohoCrmCalendarService implements Calendar {
|
|||
client_secret: this.client_secret,
|
||||
refresh_token: credentialKey.refresh_token,
|
||||
};
|
||||
const zohoCrmTokenInfo = await axios({
|
||||
method: "post",
|
||||
url: url,
|
||||
data: qs.stringify(formData),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
},
|
||||
});
|
||||
const zohoCrmTokenInfo = await refreshOAuthTokens(
|
||||
async () =>
|
||||
await axios({
|
||||
method: "post",
|
||||
url: url,
|
||||
data: qs.stringify(formData),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
},
|
||||
}),
|
||||
"zohocrm",
|
||||
credential.userId
|
||||
);
|
||||
if (!zohoCrmTokenInfo.data.error) {
|
||||
// set expiry date as offset from current time.
|
||||
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
|||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
import { getZoomAppKeys } from "../lib";
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
|
|
|
@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { getZoomAppKeys } from "../lib";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
|
|
@ -9,6 +9,9 @@ import type { CredentialPayload } from "@calcom/types/Credential";
|
|||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
import { getZoomAppKeys } from "./getZoomAppKeys";
|
||||
|
||||
/** @link https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate */
|
||||
|
@ -58,7 +61,8 @@ const zoomTokenSchema = z.object({
|
|||
|
||||
type ZoomToken = z.infer<typeof zoomTokenSchema>;
|
||||
|
||||
const isTokenValid = (token: ZoomToken) => (token.expires_in || token.expiry_date) < Date.now();
|
||||
const isTokenValid = (token: Partial<ZoomToken>) =>
|
||||
zoomTokenSchema.safeParse(token).success && (token.expires_in || token.expiry_date || 0) > Date.now();
|
||||
|
||||
/** @link https://marketplace.zoom.us/docs/guides/auth/oauth/#request */
|
||||
const zoomRefreshedTokenSchema = z.object({
|
||||
|
@ -74,17 +78,22 @@ const zoomAuth = (credential: CredentialPayload) => {
|
|||
const { client_id, client_secret } = await getZoomAppKeys();
|
||||
const authHeader = "Basic " + Buffer.from(client_id + ":" + client_secret).toString("base64");
|
||||
|
||||
const response = await fetch("https://zoom.us/oauth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
}),
|
||||
});
|
||||
const response = await refreshOAuthTokens(
|
||||
async () =>
|
||||
await fetch("https://zoom.us/oauth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
}),
|
||||
}),
|
||||
"zoomvideo",
|
||||
credential.userId
|
||||
);
|
||||
|
||||
const responseBody = await handleZoomResponse(response, credential.id);
|
||||
|
||||
|
@ -94,40 +103,32 @@ const zoomAuth = (credential: CredentialPayload) => {
|
|||
}
|
||||
}
|
||||
// We check the if the new credentials matches the expected response structure
|
||||
const parsedToken = zoomRefreshedTokenSchema.safeParse(responseBody);
|
||||
const newTokens: ParseRefreshTokenResponse<typeof zoomRefreshedTokenSchema> = parseRefreshTokenResponse(
|
||||
responseBody,
|
||||
zoomRefreshedTokenSchema
|
||||
);
|
||||
|
||||
// TODO: If the new token is invalid, initiate the fallback sequence instead of throwing
|
||||
// Expanding on this we can use server-to-server app and create meeting from admin calcom account
|
||||
if (!parsedToken.success) {
|
||||
return Promise.reject(new Error("Invalid refreshed tokens were returned"));
|
||||
}
|
||||
const newTokens = parsedToken.data;
|
||||
const oldCredential = await prisma.credential.findUniqueOrThrow({ where: { id: credential.id } });
|
||||
const parsedKey = zoomTokenSchema.safeParse(oldCredential.key);
|
||||
if (!parsedKey.success) {
|
||||
return Promise.reject(new Error("Invalid credentials were saved in the DB"));
|
||||
}
|
||||
|
||||
const key = parsedKey.data;
|
||||
key.access_token = newTokens.access_token;
|
||||
key.refresh_token = newTokens.refresh_token;
|
||||
const key = credential.key as ZoomToken;
|
||||
key.access_token = newTokens.access_token ?? key.access_token;
|
||||
key.refresh_token = (newTokens.refresh_token as string) ?? key.refresh_token;
|
||||
// set expiry date as offset from current time.
|
||||
key.expiry_date = Math.round(Date.now() + newTokens.expires_in * 1000);
|
||||
key.expiry_date =
|
||||
typeof newTokens.expires_in === "number"
|
||||
? Math.round(Date.now() + newTokens.expires_in * 1000)
|
||||
: key.expiry_date;
|
||||
// Store new tokens in database.
|
||||
await prisma.credential.update({ where: { id: credential.id }, data: { key } });
|
||||
await prisma.credential.update({
|
||||
where: { id: credential.id },
|
||||
data: { key: { ...key, ...newTokens } },
|
||||
});
|
||||
return newTokens.access_token;
|
||||
};
|
||||
|
||||
return {
|
||||
getToken: async () => {
|
||||
let credentialKey: ZoomToken | null = null;
|
||||
try {
|
||||
credentialKey = zoomTokenSchema.parse(credential.key);
|
||||
} catch (error) {
|
||||
return Promise.reject("Zoom credential keys parsing error");
|
||||
}
|
||||
const credentialKey = credential.key as ZoomToken;
|
||||
|
||||
return !isTokenValid(credentialKey)
|
||||
return isTokenValid(credentialKey)
|
||||
? Promise.resolve(credentialKey.access_token)
|
||||
: refreshAccessToken(credentialKey.refresh_token);
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@ export const featureFlagRouter = router({
|
|||
const { prisma } = ctx;
|
||||
return prisma.feature.findMany({
|
||||
orderBy: { slug: "asc" },
|
||||
cacheStrategy: { swr: 300, ttl: 300 },
|
||||
});
|
||||
}),
|
||||
map: publicProcedure.query(async ({ ctx }) => {
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { AppFlags } from "../config";
|
|||
export async function getFeatureFlagMap(prisma: PrismaClient) {
|
||||
const flags = await prisma.feature.findMany({
|
||||
orderBy: { slug: "asc" },
|
||||
cacheStrategy: { swr: 300, ttl: 300 },
|
||||
});
|
||||
return flags.reduce<AppFlags>((acc, flag) => {
|
||||
acc[flag.slug as keyof AppFlags] = flag.enabled;
|
||||
|
|
|
@ -99,3 +99,6 @@ export const ORGANIZATION_MIN_SEATS = 30;
|
|||
// Needed for emails in E2E
|
||||
export const IS_MAILHOG_ENABLED = process.env.E2E_TEST_MAILHOG_ENABLED === "1";
|
||||
export const CALCOM_VERSION = process.env.NEXT_PUBLIC_CALCOM_VERSION as string;
|
||||
|
||||
export const APP_CREDENTIAL_SHARING_ENABLED =
|
||||
process.env.CALCOM_WEBHOOK_SECRET && process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;
|
||||
|
|
|
@ -28,6 +28,15 @@ export async function getTeamWithMembers(args: {
|
|||
externalId: true,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
selectedCalendars: true,
|
||||
credentials: {
|
||||
select: {
|
||||
|
@ -68,16 +77,6 @@ export async function getTeamWithMembers(args: {
|
|||
name: true,
|
||||
logo: true,
|
||||
slug: true,
|
||||
members: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
members: {
|
||||
|
@ -142,6 +141,9 @@ export async function getTeamWithMembers(args: {
|
|||
role: obj.role,
|
||||
accepted: obj.accepted,
|
||||
disableImpersonation: obj.disableImpersonation,
|
||||
subteams: orgSlug
|
||||
? obj.user.teams.filter((obj) => obj.team.slug !== orgSlug).map((obj) => obj.team.slug)
|
||||
: null,
|
||||
avatar: `${WEBAPP_URL}/${obj.user.username}/avatar.png`,
|
||||
connectedApps: obj?.user?.credentials?.map((cred) => {
|
||||
const appSlug = cred.app?.slug;
|
||||
|
|
|
@ -197,10 +197,14 @@
|
|||
"BASECAMP3_USER_AGENT",
|
||||
"AUTH_BEARER_TOKEN_VERCEL",
|
||||
"BUILD_ID",
|
||||
"CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY",
|
||||
"CALCOM_CREDENTIAL_SYNC_ENDPOINT",
|
||||
"CALCOM_ENV",
|
||||
"CALCOM_LICENSE_KEY",
|
||||
"CALCOM_TELEMETRY_DISABLED",
|
||||
"CALCOM_WEBHOOK_HEADER_NAME",
|
||||
"CALENDSO_ENCRYPTION_KEY",
|
||||
"CALCOM_WEBHOOK_SECRET",
|
||||
"CI",
|
||||
"CLOSECOM_API_KEY",
|
||||
"CRON_API_KEY",
|
||||
|
|
Loading…
Reference in New Issue