-
Notifications
You must be signed in to change notification settings - Fork 8.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Single domain setup when only one org in the system #18383
Changes from all commits
e25fe05
7b00ce1
0cf6628
7d7905a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
const isSingleOrgModeEnabled = !!process.env.NEXT_PUBLIC_SINGLE_ORG_SLUG; | ||
const orgSlugCaptureGroupName = "orgSlug"; | ||
/** | ||
* Returns the leftmost subdomain from a given URL. | ||
* It needs the URL domain to have atleast two dots. | ||
* app.cal.com -> app | ||
* app.company.cal.com -> app | ||
* app.company.com -> app | ||
*/ | ||
const getLeftMostSubdomain = (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; | ||
}; | ||
|
||
const getRegExpNotMatchingLeftMostSubdomain = (url) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed |
||
const leftMostSubdomain = getLeftMostSubdomain(url); | ||
const subdomain = leftMostSubdomain ? `(?!${leftMostSubdomain})[^.]+` : "[^.]+"; | ||
return subdomain; | ||
}; | ||
|
||
// For app.cal.com, it will match all domains that are not starting with "app". Technically we would want to match domains like acme.cal.com, dunder.cal.com and not app.cal.com | ||
const getRegExpThatMatchesAllOrgDomains = (exports.getRegExpThatMatchesAllOrgDomains = ({ webAppUrl }) => { | ||
if (isSingleOrgModeEnabled) { | ||
console.log("Single-Org-Mode enabled - Consider all domains to be org domains"); | ||
// It works in combination with next.config.js where in this case we use orgSlug=NEXT_PUBLIC_SINGLE_ORG_SLUG | ||
return `.*`; | ||
} | ||
const subdomainRegExp = getRegExpNotMatchingLeftMostSubdomain(webAppUrl); | ||
return `^(?<${orgSlugCaptureGroupName}>${subdomainRegExp})\\.(?!vercel\.app).*`; | ||
}); | ||
|
||
const nextJsOrgRewriteConfig = { | ||
// :orgSlug is special value which would get matching group from the regex in orgHostPath | ||
orgSlug: process.env.NEXT_PUBLIC_SINGLE_ORG_SLUG || `:${orgSlugCaptureGroupName}`, | ||
orgHostPath: getRegExpThatMatchesAllOrgDomains({ | ||
webAppUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || `https://${process.env.VERCEL_URL}`, | ||
}), | ||
// We disable root path rewrite because we want to serve dashboard on root path | ||
disableRootPathRewrite: isSingleOrgModeEnabled, | ||
}; | ||
|
||
exports.nextJsOrgRewriteConfig = nextJsOrgRewriteConfig; |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,12 +7,11 @@ const { withSentryConfig } = require("@sentry/nextjs"); | |
const { version } = require("./package.json"); | ||
const { i18n } = require("./next-i18next.config"); | ||
const { | ||
orgHostPath, | ||
nextJsOrgRewriteConfig, | ||
orgUserRoutePath, | ||
orgUserTypeRoutePath, | ||
orgUserTypeEmbedRoutePath, | ||
} = require("./pagesAndRewritePaths"); | ||
|
||
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"); | ||
const isOrganizationsEnabled = | ||
|
@@ -119,55 +118,60 @@ if (process.env.ANALYZE === "true") { | |
} | ||
|
||
plugins.push(withAxiom); | ||
const orgDomainMatcherConfig = { | ||
root: nextJsOrgRewriteConfig.disableRootPathRewrite | ||
? null | ||
: { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: nextJsOrgRewriteConfig.orgHostPath, | ||
}, | ||
], | ||
source: "/", | ||
}, | ||
|
||
const matcherConfigRootPath = { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: orgHostPath, | ||
}, | ||
], | ||
source: "/", | ||
}; | ||
|
||
const matcherConfigRootPathEmbed = { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: orgHostPath, | ||
}, | ||
], | ||
Comment on lines
-123
to
-139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clubbed them under a single object. |
||
source: "/embed", | ||
}; | ||
rootEmbed: nextJsOrgRewriteConfig.disableRootEmbedPathRewrite | ||
? null | ||
: { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: nextJsOrgRewriteConfig.orgHostPath, | ||
}, | ||
], | ||
source: "/embed", | ||
}, | ||
|
||
const matcherConfigUserRoute = { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: orgHostPath, | ||
}, | ||
], | ||
source: orgUserRoutePath, | ||
}; | ||
user: { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: nextJsOrgRewriteConfig.orgHostPath, | ||
}, | ||
], | ||
source: orgUserRoutePath, | ||
}, | ||
|
||
const matcherConfigUserTypeRoute = { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: orgHostPath, | ||
}, | ||
], | ||
source: orgUserTypeRoutePath, | ||
}; | ||
userType: { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: nextJsOrgRewriteConfig.orgHostPath, | ||
}, | ||
], | ||
source: orgUserTypeRoutePath, | ||
}, | ||
|
||
const matcherConfigUserTypeEmbedRoute = { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: orgHostPath, | ||
}, | ||
], | ||
source: orgUserTypeEmbedRoutePath, | ||
userTypeEmbed: { | ||
has: [ | ||
{ | ||
type: "host", | ||
value: nextJsOrgRewriteConfig.orgHostPath, | ||
}, | ||
], | ||
source: orgUserTypeEmbedRoutePath, | ||
}, | ||
}; | ||
|
||
/** @type {import("next").NextConfig} */ | ||
|
@@ -287,6 +291,7 @@ const nextConfig = { | |
return config; | ||
}, | ||
async rewrites() { | ||
const { orgSlug } = nextJsOrgRewriteConfig; | ||
const beforeFiles = [ | ||
{ | ||
source: "/forms/:formQuery*", | ||
|
@@ -322,29 +327,33 @@ const nextConfig = { | |
// 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 | ||
? [ | ||
orgDomainMatcherConfig.root | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allows to conditionally enable rewrite to organization for root path. We disable it for single domain setup of org |
||
? { | ||
...orgDomainMatcherConfig.root, | ||
destination: `/team/${orgSlug}?isOrgProfile=1`, | ||
} | ||
: null, | ||
orgDomainMatcherConfig.rootEmbed | ||
? { | ||
...orgDomainMatcherConfig.rootEmbed, | ||
destination: `/team/${orgSlug}/embed?isOrgProfile=1`, | ||
} | ||
: null, | ||
{ | ||
...matcherConfigRootPath, | ||
destination: "/team/:orgSlug?isOrgProfile=1", | ||
...orgDomainMatcherConfig.user, | ||
destination: `/org/${orgSlug}/:user`, | ||
}, | ||
{ | ||
...matcherConfigRootPathEmbed, | ||
destination: "/team/:orgSlug/embed?isOrgProfile=1", | ||
...orgDomainMatcherConfig.userType, | ||
destination: `/org/${orgSlug}/:user/:type`, | ||
}, | ||
{ | ||
...matcherConfigUserRoute, | ||
destination: "/org/:orgSlug/:user", | ||
}, | ||
{ | ||
...matcherConfigUserTypeRoute, | ||
destination: "/org/:orgSlug/:user/:type", | ||
}, | ||
{ | ||
...matcherConfigUserTypeEmbedRoute, | ||
destination: "/org/:orgSlug/:user/:type/embed", | ||
...orgDomainMatcherConfig.userTypeEmbed, | ||
destination: `/org/${orgSlug}/:user/:type/embed`, | ||
}, | ||
] | ||
: []), | ||
]; | ||
].filter(Boolean); | ||
|
||
let afterFiles = [ | ||
{ | ||
|
@@ -388,6 +397,7 @@ const nextConfig = { | |
}; | ||
}, | ||
async headers() { | ||
const { orgSlug } = nextJsOrgRewriteConfig; | ||
// This header can be set safely as it ensures the browser will load the resources even when COEP is set. | ||
// But this header must be set only on those resources that are safe to be loaded in a cross-origin context e.g. all embeddable pages's resources | ||
const CORP_CROSS_ORIGIN_HEADER = { | ||
|
@@ -474,45 +484,47 @@ const nextConfig = { | |
], | ||
...(isOrganizationsEnabled | ||
? [ | ||
orgDomainMatcherConfig.root | ||
? { | ||
...orgDomainMatcherConfig.root, | ||
headers: [ | ||
{ | ||
key: "X-Cal-Org-path", | ||
value: `/team/${orgSlug}`, | ||
}, | ||
], | ||
} | ||
: null, | ||
{ | ||
...matcherConfigRootPath, | ||
headers: [ | ||
{ | ||
key: "X-Cal-Org-path", | ||
value: "/team/:orgSlug", | ||
}, | ||
], | ||
}, | ||
{ | ||
...matcherConfigUserRoute, | ||
...orgDomainMatcherConfig.user, | ||
headers: [ | ||
{ | ||
key: "X-Cal-Org-path", | ||
value: "/org/:orgSlug/:user", | ||
value: `/org/${orgSlug}/:user`, | ||
}, | ||
], | ||
}, | ||
{ | ||
...matcherConfigUserTypeRoute, | ||
...orgDomainMatcherConfig.userType, | ||
headers: [ | ||
{ | ||
key: "X-Cal-Org-path", | ||
value: "/org/:orgSlug/:user/:type", | ||
value: `/org/${orgSlug}/:user/:type`, | ||
}, | ||
], | ||
}, | ||
{ | ||
...matcherConfigUserTypeEmbedRoute, | ||
...orgDomainMatcherConfig.userTypeEmbed, | ||
headers: [ | ||
{ | ||
key: "X-Cal-Org-path", | ||
value: "/org/:orgSlug/:user/:type/embed", | ||
value: `/org/${orgSlug}/:user/:type/embed`, | ||
}, | ||
], | ||
}, | ||
] | ||
: []), | ||
]; | ||
].filter(Boolean); | ||
}, | ||
async redirects() { | ||
const redirects = [ | ||
|
@@ -592,7 +604,7 @@ const nextConfig = { | |
{ | ||
type: "header", | ||
key: "host", | ||
value: orgHostPath, | ||
value: nextJsOrgRewriteConfig.orgHostPath, | ||
}, | ||
], | ||
destination: "/event-types?openPlain=true", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed
getDefaultSubdomain
->getLeftMostSubdomain