Compare commits

...
6 Commits
Author SHA1 Message Date
Keith Williams b9ced0ecf8 v3.0.9
release.yml / release (push) Canceled after 0s
2023-06-28 13:54:45 +02:00
Efraín Rochín 04ed36cfcf fix: Slot reservation in new booker (#9769)
## What does this PR do?

Bring back the slot reservation sistem to the new booker

## Type of change

<!-- Please delete bullets that are not relevant. -->

- Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->

### Seated event
- [x] Open the new booker in an event with seats 
- [x] Select an available slot
- [x] Open the same event and date in other browser, the seat should be already reserved.
- [x] In the first browser come back to the slots views or close the booking form
- [x] In the second browser reload the booking page, the reserved slot should be realeased. 

### No seated event
- [x] Open the new booker
- [x] Select an available slot
- [x] Open the same event and date in other browser, the seat should be disabled


## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2023-06-27 16:34:15 -07:00
Leo Giovanettiandzomars 467cacb8da fix: Make sure we get slug to decide things (#9727)
Co-authored-by: zomars <zomars@me.com>
2023-06-27 20:45:18 +00:00
b799fdcb7e chore: Subdomain blacklist when creating org (#9728)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: Omar López <zomars@me.com>
2023-06-27 20:32:19 +00:00
Crowdin Bot d8017e4d2d New Crowdin translations by Github Action 2023-06-27 19:02:20 +00:00
Hariom Balhara 8384c3f9ff test: Hotfix/Orgs rewrite causing 404 with 3 or more dots domains (#9803)
* Fix RegExp

* Used ORGANIZATIONS_ENABLED to opt-out of orgs behaviour

* Add comments

* Add a new testcase

* Add response headers for easier debugging and Checkly tests in production
2023-06-27 12:01:30 -07:00
24 changed files with 252 additions and 133 deletions
+6 -2
View File
@@ -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
+15
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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": "Принаймні один робочий процес активний у цьому типі заходів, який надсилає електронний лист учасникам у разі бронювання заходу.",
+35
View File
@@ -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),
};
}
+19 -10
View File
@@ -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);
});
})
});
});
+4
View File
@@ -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[];
+2 -1
View File
@@ -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,
};
}
}
}
+2
View File
@@ -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",