Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9ced0ecf8 | ||
|
|
04ed36cfcf | ||
|
|
467cacb8da | ||
|
|
b799fdcb7e | ||
|
|
d8017e4d2d | ||
|
|
8384c3f9ff |
+6
-2
@@ -59,6 +59,11 @@ SAML_CLIENT_SECRET_VERIFIER=
|
||||
# PGSSLMODE='no-verify'
|
||||
PGSSLMODE=
|
||||
|
||||
# Define which hostnames are expected for the app to work on
|
||||
ALLOWED_HOSTNAMES='"cal.com","cal.dev","cal-staging.com","cal.community","cal.local:3000","localhost:3000"'
|
||||
# Reserved orgs subdomains for our own usage
|
||||
RESERVED_SUBDOMAINS='"app","auth","docs","design","console","go","status","api","saml","www","matrix","developer","cal","my","team","support","security","blog","learn","admin"'
|
||||
|
||||
# - NEXTAUTH
|
||||
# @see: https://github.com/calendso/calendso/issues/263
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_url
|
||||
@@ -172,7 +177,6 @@ CLOSECOM_API_KEY=
|
||||
# Sendgrid internal sync service
|
||||
SENDGRID_SYNC_API_KEY=
|
||||
|
||||
|
||||
# Change your Brand
|
||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
|
||||
@@ -210,4 +214,4 @@ AUTH_BEARER_TOKEN_VERCEL=
|
||||
NEW_BOOKER_ENABLED_FOR_EMBED=0
|
||||
|
||||
#Enables New booker for All but Embed requests
|
||||
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
|
||||
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
const getDefaultSubdomain = (url) => {
|
||||
if (!url.startsWith("http:") && !url.startsWith("https:")) {
|
||||
// Make it a valid URL. Mabe we can simply return null and opt-out from orgs support till the use a URL scheme.
|
||||
url = `https://${url}`;
|
||||
}
|
||||
const _url = new URL(url);
|
||||
const regex = new RegExp(/^([a-z]+\:\/{2})?((?<subdomain>[\w-.]+)\.[\w-]+\.\w+)$/);
|
||||
//console.log(_url.hostname, _url.hostname.match(regex));
|
||||
return _url.hostname.match(regex)?.groups?.subdomain || null;
|
||||
};
|
||||
exports.getSubdomainRegExp = (url) => {
|
||||
const defaultSubdomain = getDefaultSubdomain(url);
|
||||
const subdomain = defaultSubdomain ? `(?!${defaultSubdomain})[^.]+` : "[^.]+";
|
||||
return subdomain;
|
||||
};
|
||||
+79
-41
@@ -5,6 +5,7 @@ const englishTranslation = require("./public/static/locales/en/common.json");
|
||||
const { withAxiom } = require("next-axiom");
|
||||
const { i18n } = require("./next-i18next.config");
|
||||
const { pages } = require("./pages");
|
||||
const { getSubdomainRegExp } = require("./getSubdomainRegExp");
|
||||
|
||||
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
|
||||
@@ -70,14 +71,6 @@ const informAboutDuplicateTranslations = () => {
|
||||
};
|
||||
|
||||
informAboutDuplicateTranslations();
|
||||
|
||||
const getSubdomain = () => {
|
||||
const _url = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL);
|
||||
const regex = new RegExp(/^([a-z]+\:\/{2})?((?<subdomain>[\w-]+)\.[\w-]+\.\w+)$/);
|
||||
//console.log(_url.hostname, _url.hostname.match(regex));
|
||||
return _url.hostname.match(regex)?.groups?.subdomain || null;
|
||||
};
|
||||
|
||||
const plugins = [];
|
||||
if (process.env.ANALYZE === "true") {
|
||||
// only load dependency if env `ANALYZE` was set
|
||||
@@ -102,6 +95,39 @@ const teamTypeRouteRegExp = "/team/:slug/:type((?!book$)[^/]+)";
|
||||
const privateLinkRouteRegExp = "/d/:link/:slug((?!book$)[^/]+)";
|
||||
const embedUserTypeRouteRegExp = `/:user((?!${pages.join("/|")})[^/]*)/:type/embed`;
|
||||
const embedTeamTypeRouteRegExp = "/team/:slug/:type/embed";
|
||||
const subdomainRegExp = getSubdomainRegExp(process.env.NEXT_PUBLIC_WEBAPP_URL);
|
||||
// Important Note: Do update the RegExp in apps/web/test/lib/next-config.test.ts when changing it.
|
||||
const orgHostRegExp = `^(?<orgSlug>${subdomainRegExp})\\..*`;
|
||||
|
||||
const matcherConfigRootPath = {
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: orgHostRegExp,
|
||||
},
|
||||
],
|
||||
source: "/",
|
||||
};
|
||||
|
||||
const matcherConfigOrgMemberPath = {
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: orgHostRegExp,
|
||||
},
|
||||
],
|
||||
source: `/:user((?!${pages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`,
|
||||
};
|
||||
|
||||
const matcherConfigUserPath = {
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: `^(?<orgSlug>${subdomainRegExp}[^.]+)\\..*`,
|
||||
},
|
||||
],
|
||||
source: `/:user((?!${pages.join("|")}|_next|public))/:path*`,
|
||||
};
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
@@ -193,40 +219,23 @@ const nextConfig = {
|
||||
return config;
|
||||
},
|
||||
async rewrites() {
|
||||
const defaultSubdomain = getSubdomain();
|
||||
const subdomain = defaultSubdomain ? `(?!${defaultSubdomain})[^.]+` : "[^.]+";
|
||||
|
||||
const beforeFiles = [
|
||||
{
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: `^(?<orgSlug>${subdomain})\\..*`,
|
||||
},
|
||||
],
|
||||
source: "/",
|
||||
destination: "/team/:orgSlug",
|
||||
},
|
||||
{
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: `^(?<orgSlug>${subdomain})\\..*`,
|
||||
},
|
||||
],
|
||||
source: `/:user((?!${pages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`,
|
||||
destination: "/org/:orgSlug/:user",
|
||||
},
|
||||
{
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: `^(?<orgSlug>${subdomain}[^.]+)\\..*`,
|
||||
},
|
||||
],
|
||||
source: `/:user((?!${pages.join("|")}|_next|public))/:path*`,
|
||||
destination: "/:user/:path*",
|
||||
},
|
||||
...(process.env.ORGANIZATIONS_ENABLED
|
||||
? [
|
||||
{
|
||||
...matcherConfigRootPath,
|
||||
destination: "/team/:orgSlug",
|
||||
},
|
||||
{
|
||||
...matcherConfigOrgMemberPath,
|
||||
destination: "/org/:orgSlug/:user",
|
||||
},
|
||||
{
|
||||
...matcherConfigUserPath,
|
||||
destination: "/:user/:path*",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
let afterFiles = [
|
||||
@@ -381,6 +390,35 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
...[
|
||||
{
|
||||
...matcherConfigRootPath,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/team/:orgSlug",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...matcherConfigOrgMemberPath,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/org/:orgSlug/:user",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...matcherConfigUserPath,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/:user/:path",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.0.8",
|
||||
"version": "3.0.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"manage_this_event": "قم بإدارة هذا الحدث",
|
||||
"invite_team_member": "دعوة عضو في الفريق",
|
||||
"invite_team_individual_segment": "دعوة فرد",
|
||||
"invite_team_bulk_segment": "استيراد بحجم جماعي",
|
||||
"invite_team_bulk_segment": "استيراد جماعي",
|
||||
"invite_team_notifcation_badge": "دعوة",
|
||||
"your_event_has_been_scheduled": "تم جدولة الحدث الخاص بك",
|
||||
"your_event_has_been_scheduled_recurring": "تم جدولة الحدث المتكرر الخاص بك",
|
||||
@@ -1736,9 +1736,9 @@
|
||||
"payment_app_commission": "يتطلب الدفع ({{paymentFeePercentage}}% + {{fee, currency}} عمولة لكل معاملة)",
|
||||
"email_invite_team": "تم دعوة {{email}}",
|
||||
"email_invite_team_bulk": "{{userCount}} مستخدم تم دعوتهم",
|
||||
"error_collecting_card": "خطأ في تحصيل البطاقة",
|
||||
"error_collecting_card": "خطأ في التحصيل من البطاقة",
|
||||
"image_size_limit_exceed": "لا ينبغي أن تتجاوز الصورة التي تم تحميلها الحد الأقصى لحجم 5 ميغابايت",
|
||||
"inline_embed": "Inline Embed",
|
||||
"inline_embed": "تضمين في تطبيقاتك",
|
||||
"load_inline_content": "يحمّل نوع الحدث الخاص بك مباشرة مع محتوى موقعك الأخير.",
|
||||
"floating_pop_up_button": "زر منبثق عائم",
|
||||
"floating_button_trigger_modal": "يضع زرًا عائمًا على موقعك يقوم بتشغيل توجيه مع نوع الحدث الخاص بك.",
|
||||
|
||||
@@ -736,7 +736,7 @@
|
||||
"new_event_type_to_book_description": "Erstellen Sie einen neuen Termintyp, mit dem Personen Zeiten buchen können.",
|
||||
"length": "Länge",
|
||||
"minimum_booking_notice": "Mindesvorlaufzeit für eine Buchung",
|
||||
"offset_toggle": "Startzeiten versetzen",
|
||||
"offset_toggle": "Versetzte Startzeiten",
|
||||
"offset_toggle_description": "Um eine bestimmte Anzahl an Minuten versetzte Zeitfenster, die Buchern angezeigt werden",
|
||||
"offset_start": "Versetzt um",
|
||||
"offset_start_description": "z.B. wird dies Zeitfenster für Ihre Bucher um {{ adjustedTime }} statt {{ originalTime }} anzeigen",
|
||||
|
||||
@@ -737,8 +737,8 @@
|
||||
"new_event_type_to_book_description": "사람들이 시간을 예약할 수 있는 새 이벤트 타입을 만듭니다.",
|
||||
"length": "길이",
|
||||
"minimum_booking_notice": "최소 고지",
|
||||
"offset_toggle": "오프셋 시작 시간",
|
||||
"offset_toggle_description": "지정된 시간(분)으로 예약자에게 표시되는 오프셋 시간 슬롯",
|
||||
"offset_toggle": "시작 시간 오프셋",
|
||||
"offset_toggle_description": "지정된 시간(분)으로 예약자에게 표시되는 시간 슬롯 오프셋",
|
||||
"offset_start": "오프셋 기준",
|
||||
"offset_start_description": "예: 이렇게 하면 예약자에게 {{ adjustedTime }}에 {{ originalTime }} 대신 시간 슬롯이 표시됨",
|
||||
"slot_interval": "시간 슬롯 간격",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"rejection_confirmation": "Boeking weigeren",
|
||||
"manage_this_event": "Deze gebeurtenis beheren",
|
||||
"invite_team_member": "Teamlid uitnodigen",
|
||||
"invite_team_individual_segment": "Individueel uitnodigen",
|
||||
"invite_team_individual_segment": "Eén persoon uitnodigen",
|
||||
"invite_team_bulk_segment": "Bulkimport",
|
||||
"invite_team_notifcation_badge": "Uitg.",
|
||||
"your_event_has_been_scheduled": "Uw gebeurtenis is gepland",
|
||||
|
||||
@@ -736,10 +736,10 @@
|
||||
"new_event_type_to_book_description": "Crie um novo tipo de evento para as pessoas agendarem horários.",
|
||||
"length": "Comprimento",
|
||||
"minimum_booking_notice": "Aviso mínimo",
|
||||
"offset_toggle": "Deslocar horas de início",
|
||||
"offset_toggle_description": "Desloca na quantidade de minutos especificada as alocações de tempo mostradas aos reservantes",
|
||||
"offset_toggle": "Deslocar horários de início",
|
||||
"offset_toggle_description": "Deslocar em uma quantidade específica de minutos as alocações de horário mostradas aos reservantes",
|
||||
"offset_start": "Deslocar em",
|
||||
"offset_start_description": "ex.: serão mostradas alocações de tempo aos seus reservantes à(s) {{ adjustedTime }} em vez de {{ originalTime }}",
|
||||
"offset_start_description": "ex.: serão mostradas aos seus reservantes alocações de horário à(s) {{ adjustedTime }} em vez de {{ originalTime }}",
|
||||
"slot_interval": "Intervalos de tempo",
|
||||
"slot_interval_default": "Usar a duração do evento (padrão)",
|
||||
"delete_event_type": "Excluir tipo de evento?",
|
||||
@@ -1738,7 +1738,7 @@
|
||||
"email_invite_team_bulk": "{{userCount}} usuários foram convidados",
|
||||
"error_collecting_card": "Erro ao coletar cartão",
|
||||
"image_size_limit_exceed": "A imagem carregada não deve ultrapassar o limite de 5 MB",
|
||||
"inline_embed": "Incorporar embutido",
|
||||
"inline_embed": "Inserção embutida",
|
||||
"load_inline_content": "Carrega seu tipo de evento embutido diretamente no conteúdo restante do site.",
|
||||
"floating_pop_up_button": "Botão pop-up flutuante",
|
||||
"floating_button_trigger_modal": "Insere um botão flutuante no seu site para ativar um modal com seu tipo de evento.",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"rejection_confirmation": "Rejeitar a reserva",
|
||||
"manage_this_event": "Gerir este evento",
|
||||
"invite_team_member": "Convidar membro da equipa",
|
||||
"invite_team_individual_segment": "Convidar indivíduo",
|
||||
"invite_team_individual_segment": "Convidar individualmente",
|
||||
"invite_team_bulk_segment": "Importação em conjunto",
|
||||
"invite_team_notifcation_badge": "Conv.",
|
||||
"your_event_has_been_scheduled": "O seu evento foi agendado",
|
||||
@@ -236,7 +236,7 @@
|
||||
"continue_without_calendar": "Continuar sem calendário",
|
||||
"connect_your_calendar": "Ligar o seu calendário",
|
||||
"connect_your_video_app": "Associar as suas aplicações de vídeo",
|
||||
"connect_your_video_app_instructions": "Associe as suas aplicações de vídeo para utilizar as mesmas nos tipos de eventos.",
|
||||
"connect_your_video_app_instructions": "Associe as suas aplicações de vídeo para utilizar as mesmas nos seus tipos de eventos.",
|
||||
"connect_your_calendar_instructions": "Ligue o seu calendário para verificar automaticamente as horas ocupadas e os novos eventos à medida que são agendados.",
|
||||
"set_up_later": "Configurar mais tarde",
|
||||
"current_time": "Hora atual",
|
||||
@@ -1737,13 +1737,13 @@
|
||||
"email_invite_team": "{{email}} foi convidado/a",
|
||||
"email_invite_team_bulk": "Foram convidados {{userCount}} utilizadores",
|
||||
"error_collecting_card": "Erro ao recolher o cartão",
|
||||
"image_size_limit_exceed": "A imagem enviada não deve exceder o limite de tamanho de 5 mb",
|
||||
"image_size_limit_exceed": "A imagem enviada não deve exceder o limite de tamanho de 5MB",
|
||||
"inline_embed": "Incorporação em linha",
|
||||
"load_inline_content": "Carrega o seu tipo de evento diretamente e em linha, em conjunto com os conteúdos do seu outro site.",
|
||||
"floating_pop_up_button": "Botão pop-up flutuante",
|
||||
"floating_button_trigger_modal": "Coloca um botão flutuante no seu site que ativa uma modal com o seu tipo de evento.",
|
||||
"pop_up_element_click": "Pop-up via clique no elemento",
|
||||
"open_dialog_with_element_click": "Abra a sua janela do Cal quando alguém clicar num elemento.",
|
||||
"open_dialog_with_element_click": "Abrir a sua janela do Cal quando alguém clicar num elemento.",
|
||||
"need_help_embedding": "Precisa de ajuda? Consulte os nossos guias para incorporar o Cal no Wix, Squarespace, ou WordPress. Consulte ainda as nossas perguntas frequentes ou explore opções avançadas de incorporação.",
|
||||
"book_my_cal": "Reservar o meu Cal",
|
||||
"invite_as": "Convidar como",
|
||||
|
||||
@@ -1755,7 +1755,7 @@
|
||||
"add_an_override": "Adaugați o suprascriere",
|
||||
"import_from_google_workspace": "Import utilizatori din Google Workspace",
|
||||
"connect_google_workspace": "Conectare la Google Workspace",
|
||||
"google_workspace_admin_tooltip": "Trebuie să fiți administrator Google Workspace pentru a putea utiliza această funcție",
|
||||
"google_workspace_admin_tooltip": "Trebuie să fiți administrator Workspace pentru a putea utiliza această funcție",
|
||||
"first_event_type_webhook_description": "Creați primul dvs. webhook pentru acest tip de eveniment",
|
||||
"create_for": "Creare pentru",
|
||||
"organization_url_taken": "Acest URL este folosit deja"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"rejection_confirmation": "Odbij rezervaciju",
|
||||
"manage_this_event": "Upravljaj ovim dogadjajem",
|
||||
"invite_team_member": "Pozovite člana tima",
|
||||
"invite_team_individual_segment": "Pozovite pojedinačno",
|
||||
"invite_team_individual_segment": "Pozovite jednu osobu",
|
||||
"invite_team_bulk_segment": "Pozovite grupno",
|
||||
"invite_team_notifcation_badge": "Poz.",
|
||||
"your_event_has_been_scheduled": "Vaš dogadjaj je odložen",
|
||||
@@ -1778,13 +1778,13 @@
|
||||
"payment_app_commission": "Zahtevajte plaćanje ({{paymentFeePercentage}}% + {{fee, currency}} provizija po transakciji)",
|
||||
"email_invite_team": "{{email}} je pozvan/a",
|
||||
"email_invite_team_bulk": "Pozvano je {{userCount}} korisnika",
|
||||
"error_collecting_card": "Greška u sakupljanju kartice",
|
||||
"error_collecting_card": "Greška u obradi kartice",
|
||||
"image_size_limit_exceed": "Otpremljena slika ne sme da prevazilazi ograničenje od 5mb",
|
||||
"unauthorized_workflow_error_message": "{{errorCode}}: Niste ovlašćeni da omogućite ili onemogućite ovaj radni tok",
|
||||
"inline_embed": "Ugradi neposredno",
|
||||
"load_inline_content": "Direktno učitava vaš tip događaja neposredno sa vašim ostalim sadržajem na veb sajtu.",
|
||||
"floating_pop_up_button": "Plutajuće iskačuće dugme",
|
||||
"floating_button_trigger_modal": "Postavlja plutajuće dugme na vaš sajt koje pokreće modal sa vašim tipom događaja.",
|
||||
"floating_button_trigger_modal": "Postavlja plutajuće dugme na vaš sajt koje pokreće modal prozor sa vašim tipom događaja.",
|
||||
"pop_up_element_click": "Iskačući prozor preko klika na element",
|
||||
"open_dialog_with_element_click": "Otvara Cal dijalog prozor kada neko klikne na element.",
|
||||
"need_help_embedding": "Potrebna vam je pomoć? Pogledajte naše vodiče za ugrađivanje Cal-a na Wix, Squarespace ili WordPress, pogledajte naša često postavljana pitanja, ili istražite napredne opcije za ugrađivanje.",
|
||||
|
||||
@@ -736,10 +736,10 @@
|
||||
"new_event_type_to_book_description": "İnsanların rezervasyon yapabileceği yeni bir etkinlik türü oluşturun.",
|
||||
"length": "Uzunluk",
|
||||
"minimum_booking_notice": "Minimum Bildirim",
|
||||
"offset_toggle": "Ofset başlangıç saatleri",
|
||||
"offset_toggle_description": "Belirli bir dakika sayısına göre rezervasyon yapanlara gösterilen ofset zaman aralıkları",
|
||||
"offset_start": "Ofset",
|
||||
"offset_start_description": "örneğin; bu işlem rezervasyon yapan kişinin zaman aralıklarını {{ originalTime }} yerine {{ adjustedTime }} olarak görmesine neden olur",
|
||||
"offset_toggle": "Başlangıç saatlerini kaydır",
|
||||
"offset_toggle_description": "Rezervasyon yapacaklara gösterilecek zaman dilimlerini belirtilen dakika sayısına göre kaydır",
|
||||
"offset_start": "Şu süreye kadar kaydır:",
|
||||
"offset_start_description": "örneğin; bu işlem, rezervasyon yapan kişilerin zaman aralıklarını {{ originalTime }} yerine {{ adjustedTime }} olarak görmesini sağlar",
|
||||
"slot_interval": "Zaman dilimi aralıkları",
|
||||
"slot_interval_default": "Etkinlik uzunluğunu kullan (varsayılan)",
|
||||
"delete_event_type": "Etkinlik türü silinsin mi?",
|
||||
@@ -1643,7 +1643,7 @@
|
||||
"switch_monthly": "Aylık görünüme geç",
|
||||
"switch_weekly": "Haftalık görünüme geç",
|
||||
"switch_multiday": "Günlük görünüme geç",
|
||||
"num_locations": "{{num}} konum seçenekleri",
|
||||
"num_locations": "{{num}} konum seçeneği",
|
||||
"select_on_next_step": "Bir sonraki adımda seç",
|
||||
"this_meeting_has_not_started_yet": "Bu toplantı henüz başlamadı",
|
||||
"this_app_requires_connected_account": "{{appName}} bağlı bir {{dependencyName}} hesabı gerektirir",
|
||||
@@ -1741,12 +1741,12 @@
|
||||
"inline_embed": "Satır İçine Yerleştir",
|
||||
"load_inline_content": "Etkinlik türünüzü doğrudan diğer web sitesi içeriğinizle birlikte yükler.",
|
||||
"floating_pop_up_button": "Kayan açılır düğme",
|
||||
"floating_button_trigger_modal": "Sitenize, bir olay türüne sahip bir modu tetikleyen kayan bir düğme ekler.",
|
||||
"floating_button_trigger_modal": "Sitenize, bir olay türüne sahip bir modeli tetikleyen kayan bir düğme ekler.",
|
||||
"pop_up_element_click": "Ögenin tıklanmasıyla açılır",
|
||||
"open_dialog_with_element_click": "Birisi bir öğeyi tıkladığında Cal iletişim kutunuzu açın.",
|
||||
"open_dialog_with_element_click": "Birisi bir ögeyi tıkladığında Cal iletişim kutunuzu açın.",
|
||||
"need_help_embedding": "Yardıma mı ihtiyacınız var? Cal'ı 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'ımı rezerve et",
|
||||
"invite_as": "Davet edin",
|
||||
"book_my_cal": "Cal hesabımda rezerve et",
|
||||
"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",
|
||||
"disable_attendees_confirmation_emails_description": "Bu etkinlik türünde etkinlik rezerve edildiğinde katılımcılara bir e-posta gönderen en az bir iş akışı etkin.",
|
||||
|
||||
@@ -1736,7 +1736,7 @@
|
||||
"payment_app_commission": "Потрібна оплата ({{paymentFeePercentage}}% + комісія за транзакцію в розмірі {{fee, currency}})",
|
||||
"email_invite_team": "{{email}} запрошено",
|
||||
"email_invite_team_bulk": "Запрошено стільки користувачів: {{userCount}}",
|
||||
"error_collecting_card": "Не вдалося забрати картку",
|
||||
"error_collecting_card": "Не вдалось опрацювати картку",
|
||||
"image_size_limit_exceed": "Розмір переданого зображення не повинен перевищувати 5 МБ",
|
||||
"inline_embed": "Вбудовування",
|
||||
"load_inline_content": "Завантажує ваш тип заходу, вбудовуючи його у ваш інший контент на вебсайті.",
|
||||
@@ -1745,8 +1745,8 @@
|
||||
"pop_up_element_click": "Спливаюче вікно після натискання на елемент",
|
||||
"open_dialog_with_element_click": "Відкриває ваш діалог із Cal, коли хтось натискає елемент.",
|
||||
"need_help_embedding": "Потрібна допомога? Перегляньте наші посібники з вбудовування Cal на Wix, Squarespace або WordPress, ознайомтеся з поширеними запитаннями або розширеними параметрами вбудовування.",
|
||||
"book_my_cal": "Забронювати мій Cal",
|
||||
"invite_as": "Запросити як",
|
||||
"book_my_cal": "Забронювати на Cal.com",
|
||||
"invite_as": "Запросити в такій ролі:",
|
||||
"form_updated_successfully": "Форму оновлено.",
|
||||
"disable_attendees_confirmation_emails": "Вимкнути листи підтвердження за замовчуванням для учасників",
|
||||
"disable_attendees_confirmation_emails_description": "Принаймні один робочий процес активний у цьому типі заходів, який надсилає електронний лист учасникам у разі бронювання заходу.",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
import { it, expect, describe, beforeAll, afterAll } from "vitest";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { getSubdomainRegExp } = require("../../getSubdomainRegExp");
|
||||
let userTypeRouteRegExp: RegExp;
|
||||
let teamTypeRouteRegExp:RegExp;
|
||||
let privateLinkRouteRegExp:RegExp
|
||||
@@ -161,3 +163,36 @@ describe('next.config.js - RegExp', ()=>{
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('next.config.js - Org Rewrite', ()=> {
|
||||
// RegExp copied from next.config.js
|
||||
const orgHostRegExp = (subdomainRegExp:string)=> new RegExp(`^(?<orgSlug>${subdomainRegExp})\\..*`)
|
||||
describe('SubDomain Retrieval from NEXT_PUBLIC_WEBAPP_URL', ()=>{
|
||||
it('https://app.cal.com', ()=>{
|
||||
const subdomainRegExp = getSubdomainRegExp('https://app.cal.com');
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('app.cal.com')).toEqual(null)
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('company.app.cal.com')?.groups?.orgSlug).toEqual('company')
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('org.cal.com')?.groups?.orgSlug).toEqual('org')
|
||||
})
|
||||
|
||||
it('app.cal.com', ()=>{
|
||||
const subdomainRegExp = getSubdomainRegExp('app.cal.com');
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('app.cal.com')).toEqual(null)
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('company.app.cal.com')?.groups?.orgSlug).toEqual('company')
|
||||
})
|
||||
|
||||
it('https://calcom.app.company.com', ()=>{
|
||||
const subdomainRegExp = getSubdomainRegExp('https://calcom.app.company.com');
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('calcom.app.company.com')).toEqual(null)
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('acme.calcom.app.company.com')?.groups?.orgSlug).toEqual('acme')
|
||||
})
|
||||
|
||||
it('https://calcom.example.com', ()=>{
|
||||
const subdomainRegExp = getSubdomainRegExp('https://calcom.example.com');
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('calcom.example.com')).toEqual(null)
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('acme.calcom.example.com')?.groups?.orgSlug).toEqual('acme')
|
||||
// The following also matches which causes anything other than the domain in NEXT_PUBLIC_WEBAPP_URL to give 404
|
||||
expect(orgHostRegExp(subdomainRegExp).exec('some-other.company.com')?.groups?.orgSlug).toEqual('some-other')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,6 @@ import dayjs from "@calcom/dayjs";
|
||||
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
|
||||
import { useSlotsForMultipleDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useBookerStore } from "../store";
|
||||
import { useEvent, useScheduleForEvent } from "../utils/event";
|
||||
@@ -23,9 +22,7 @@ type AvailableTimeSlotsProps = {
|
||||
* in columns next to each other.
|
||||
*/
|
||||
export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeslot }: AvailableTimeSlotsProps) => {
|
||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
const duration = useBookerStore((state) => state.selectedDuration);
|
||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||
const event = useEvent();
|
||||
const date = selectedDate || dayjs().format("YYYY-MM-DD");
|
||||
@@ -34,14 +31,6 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeslot }:
|
||||
setSelectedTimeslot(time);
|
||||
|
||||
if (!event.data) return;
|
||||
reserveSlotMutation.mutate({
|
||||
slotUtcStartDate: time,
|
||||
eventTypeId: event.data.id,
|
||||
slotUtcEndDate: dayjs(time)
|
||||
.utc()
|
||||
.add(duration || event.data.length, "minutes")
|
||||
.format(),
|
||||
});
|
||||
};
|
||||
|
||||
const schedule = useScheduleForEvent({
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import type { FieldError } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
@@ -23,8 +23,10 @@ import getBookingResponsesSchema, {
|
||||
getBookingResponsesPartialSchema,
|
||||
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Form, Button, Alert, EmptyScreen } from "@calcom/ui";
|
||||
import { Calendar } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -38,6 +40,12 @@ type BookEventFormProps = {
|
||||
};
|
||||
|
||||
export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation({
|
||||
trpc: { context: { skipBatch: true } },
|
||||
});
|
||||
const releaseSlotMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation({
|
||||
trpc: { context: { skipBatch: true } },
|
||||
});
|
||||
const router = useRouter();
|
||||
const { t, i18n } = useLocale();
|
||||
const { timezone } = useTimePreferences();
|
||||
@@ -55,6 +63,30 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
||||
const event = useEvent();
|
||||
const eventType = event.data;
|
||||
|
||||
const reserveSlot = () => {
|
||||
if (eventType) {
|
||||
reserveSlotMutation.mutate({
|
||||
slotUtcStartDate: dayjs(timeslot).utc().format(),
|
||||
eventTypeId: eventType.id,
|
||||
slotUtcEndDate: dayjs(timeslot)
|
||||
.utc()
|
||||
.add(duration || eventType.length, "minutes")
|
||||
.format(),
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
reserveSlot();
|
||||
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
|
||||
return () => {
|
||||
if (eventType) {
|
||||
releaseSlotMutation.mutate();
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [eventType]);
|
||||
|
||||
const defaultValues = useMemo(() => {
|
||||
if (Object.keys(formValues).length) return formValues;
|
||||
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
// Define which hostnames are expected for the app
|
||||
export const appHostnames = [
|
||||
"cal.com",
|
||||
"cal.dev",
|
||||
"cal-staging.com",
|
||||
"cal.community",
|
||||
"cal.local:3000",
|
||||
// ⬇️ Prevents 404 error for normal localhost development, makes it backwards compatible
|
||||
"localhost:3000",
|
||||
];
|
||||
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
/**
|
||||
* return the org slug
|
||||
@@ -17,7 +6,7 @@ export const appHostnames = [
|
||||
*/
|
||||
export function getOrgDomain(hostname: string) {
|
||||
// Find which hostname is being currently used
|
||||
const currentHostname = appHostnames.find((ahn) => {
|
||||
const currentHostname = ALLOWED_HOSTNAMES.find((ahn) => {
|
||||
const url = new URL(WEBAPP_URL);
|
||||
const testHostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`;
|
||||
return testHostname.endsWith(`.${ahn}`);
|
||||
@@ -34,8 +23,7 @@ export function orgDomainConfig(hostname: string) {
|
||||
const currentOrgDomain = getOrgDomain(hostname);
|
||||
return {
|
||||
currentOrgDomain,
|
||||
isValidOrgDomain:
|
||||
currentOrgDomain !== null && currentOrgDomain !== "app" && !appHostnames.includes(currentOrgDomain),
|
||||
isValidOrgDomain: currentOrgDomain !== null && !RESERVED_SUBDOMAINS.includes(currentOrgDomain),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { orgDomainConfig, getOrgDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
|
||||
import * as constants from "@calcom/lib/constants";
|
||||
|
||||
function setupEnvs({ WEBAPP_URL = "https://app.cal.com" } = {}) {
|
||||
Object.defineProperty(constants, "WEBAPP_URL", { value: WEBAPP_URL });
|
||||
Object.defineProperty(constants, "ALLOWED_HOSTNAMES", {
|
||||
value: ["cal.com", "cal.dev", "cal-staging.com", "cal.community", "cal.local:3000", "localhost:3000"],
|
||||
});
|
||||
Object.defineProperty(constants, "RESERVED_SUBDOMAINS", {
|
||||
value: [ "app", "auth", "docs", "design", "console", "go", "status", "api", "saml", "www", "matrix", "developer", "cal", "my", "team", "support", "security", "blog", "learn", "admin"],
|
||||
});
|
||||
}
|
||||
|
||||
describe("Org Domains Utils", () => {
|
||||
describe("orgDomainConfig", () => {
|
||||
it("should return a valid org domain", () => {
|
||||
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
|
||||
setupEnvs();
|
||||
expect(orgDomainConfig("acme.cal.com")).toEqual({
|
||||
currentOrgDomain: "acme",
|
||||
isValidOrgDomain: true
|
||||
isValidOrgDomain: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return a non valid org domain", () => {
|
||||
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
|
||||
setupEnvs();
|
||||
expect(orgDomainConfig("app.cal.com")).toEqual({
|
||||
currentOrgDomain: "app",
|
||||
isValidOrgDomain: false
|
||||
isValidOrgDomain: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrgDomain", () => {
|
||||
it("should handle a prod web app url with a prod subdomain hostname", () => {
|
||||
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
|
||||
setupEnvs();
|
||||
expect(getOrgDomain("acme.cal.com")).toEqual("acme");
|
||||
});
|
||||
|
||||
it("should handle a prod web app url with a staging subdomain hostname", () => {
|
||||
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
|
||||
setupEnvs();
|
||||
expect(getOrgDomain("acme.cal.dev")).toEqual(null);
|
||||
});
|
||||
|
||||
it("should handle a local web app with port url with a local subdomain hostname", () => {
|
||||
Object.defineProperty(constants, 'WEBAPP_URL', {value:"http://app.cal.local:3000"});
|
||||
setupEnvs({ WEBAPP_URL: "http://app.cal.local:3000" });
|
||||
expect(getOrgDomain("acme.cal.local:3000")).toEqual("acme");
|
||||
});
|
||||
|
||||
it("should handle a local web app with port url with a non-local subdomain hostname", () => {
|
||||
Object.defineProperty(constants, 'WEBAPP_URL', {value:"http://app.cal.local:3000"});
|
||||
setupEnvs({ WEBAPP_URL: "http://app.cal.local:3000" });
|
||||
expect(getOrgDomain("acme.cal.com:3000")).toEqual(null);
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,3 +70,7 @@ export const IS_STRIPE_ENABLED = !!(
|
||||
export const IS_TEAM_BILLING_ENABLED = IS_STRIPE_ENABLED && (!IS_SELF_HOSTED || HOSTED_CAL_FEATURES);
|
||||
export const FULL_NAME_LENGTH_MAX_LIMIT = 50;
|
||||
export const MINUTES_TO_BOOK = process.env.NEXT_PUBLIC_MINUTES_TO_BOOK || "5";
|
||||
|
||||
// Needed for orgs
|
||||
export const ALLOWED_HOSTNAMES = JSON.parse(`[${process.env.ALLOWED_HOSTNAMES || ""}]`) as string[];
|
||||
export const RESERVED_SUBDOMAINS = JSON.parse(`[${process.env.RESERVED_SUBDOMAINS || ""}]`) as string[];
|
||||
|
||||
@@ -22,7 +22,8 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
|
||||
const defaultOptions: CookieOption["options"] = {
|
||||
domain: isENVDev
|
||||
? process.env.ORGANIZATIONS_ENABLED
|
||||
? ".cal.local"
|
||||
? //FIXME: This is causing login to not work if someone uses anything other .cal.local for testing
|
||||
".cal.local"
|
||||
: undefined
|
||||
: NEXTAUTH_COOKIE_DOMAIN,
|
||||
// To enable cookies on widgets,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { totp } from "otplib";
|
||||
import { sendOrganizationEmailVerification } from "@calcom/emails";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { IS_PRODUCTION, IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { IS_PRODUCTION, IS_TEAM_BILLING_ENABLED, RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
@@ -66,7 +66,8 @@ export const createHandler = async ({ input }: CreateOptions) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (slugCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "organization_url_taken" });
|
||||
if (slugCollisions || RESERVED_SUBDOMAINS.includes(slug))
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "organization_url_taken" });
|
||||
if (userCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "admin_email_taken" });
|
||||
|
||||
const password = createHash("md5")
|
||||
|
||||
@@ -66,26 +66,27 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
timeFormat: input.timeFormat,
|
||||
};
|
||||
|
||||
if (
|
||||
input.slug &&
|
||||
IS_TEAM_BILLING_ENABLED &&
|
||||
/** If the team doesn't have a slug we can assume that it hasn't been published yet. */
|
||||
!prevOrganisation.slug
|
||||
) {
|
||||
// Save it on the metadata so we can use it later
|
||||
data.metadata = {
|
||||
requestedSlug: input.slug,
|
||||
};
|
||||
} else {
|
||||
data.slug = input.slug;
|
||||
|
||||
// If we save slug, we don't need the requestedSlug anymore
|
||||
const metadataParse = teamMetadataSchema.safeParse(prevOrganisation.metadata);
|
||||
if (metadataParse.success) {
|
||||
const { requestedSlug: _, ...cleanMetadata } = metadataParse.data || {};
|
||||
if (input.slug) {
|
||||
if (
|
||||
IS_TEAM_BILLING_ENABLED &&
|
||||
/** If the team doesn't have a slug we can assume that it hasn't been published yet. */
|
||||
!prevOrganisation.slug
|
||||
) {
|
||||
// Save it on the metadata so we can use it later
|
||||
data.metadata = {
|
||||
...cleanMetadata,
|
||||
requestedSlug: input.slug,
|
||||
};
|
||||
} else {
|
||||
data.slug = input.slug;
|
||||
|
||||
// If we save slug, we don't need the requestedSlug anymore
|
||||
const metadataParse = teamMetadataSchema.safeParse(prevOrganisation.metadata);
|
||||
if (metadataParse.success) {
|
||||
const { requestedSlug: _, ...cleanMetadata } = metadataParse.data || {};
|
||||
data.metadata = {
|
||||
...cleanMetadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@
|
||||
},
|
||||
"globalDependencies": ["yarn.lock"],
|
||||
"globalEnv": [
|
||||
"ALLOWED_HOSTNAMES",
|
||||
"ANALYZE",
|
||||
"API_KEY_PREFIX",
|
||||
"APP_USER_NAME",
|
||||
@@ -246,6 +247,7 @@
|
||||
"QUICK",
|
||||
"RAILWAY_STATIC_URL",
|
||||
"RENDER_EXTERNAL_URL",
|
||||
"RESERVED_SUBDOMAINS",
|
||||
"SALESFORCE_CONSUMER_KEY",
|
||||
"SALESFORCE_CONSUMER_SECRET",
|
||||
"SAML_ADMINS",
|
||||
|
||||
Reference in New Issue
Block a user