Merge branch 'feat/organizations' into feat/organizations-banner
This commit is contained in:
commit
5a9caa4cc6
11
.env.example
11
.env.example
|
@ -32,11 +32,11 @@ PRISMA_GENERATE_DATAPROXY=
|
|||
# ***********************************************************************************************************
|
||||
|
||||
# - SHARED **************************************************************************************************
|
||||
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||
NEXT_PUBLIC_WEBAPP_URL='http://app.cal.local:3000'
|
||||
# Change to 'http://localhost:3001' if running the website simultaneously
|
||||
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
||||
NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
|
||||
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
|
||||
NEXT_PUBLIC_EMBED_LIB_URL='http://app.cal.local:3000/embed/embed.js'
|
||||
|
||||
# To enable SAML login, set both these variables
|
||||
# @see https://github.com/calcom/cal.com/tree/main/packages/features/ee#setting-up-saml-login
|
||||
|
@ -58,7 +58,7 @@ PGSSLMODE=
|
|||
# @see: https://github.com/calendso/calendso/issues/263
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_url
|
||||
# Required for Vercel hosting - set NEXTAUTH_URL to equal your NEXT_PUBLIC_WEBAPP_URL
|
||||
NEXTAUTH_URL='http://localhost:3000'
|
||||
NEXTAUTH_URL='http://app.cal.local:3000'
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
NEXTAUTH_SECRET=
|
||||
|
@ -183,3 +183,8 @@ CSP_POLICY=
|
|||
EDGE_CONFIG=
|
||||
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
|
||||
# Vercel
|
||||
PROJECT_ID_VERCEL=
|
||||
TEAM_ID_VERCEL=
|
||||
AUTH_BEARER_TOKEN_VERCEL=
|
||||
|
|
|
@ -3,6 +3,7 @@ import { debounce, noop } from "lodash";
|
|||
import type { RefCallback } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
|
@ -31,6 +32,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||
usernameRef,
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
user,
|
||||
} = props;
|
||||
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
||||
const [markAsError, setMarkAsError] = useState(false);
|
||||
|
@ -108,6 +110,12 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||
});
|
||||
};
|
||||
|
||||
const usernamePrefix = user.organization
|
||||
? user.organization.slug
|
||||
? `${user.organization.slug}.${subdomainSuffix()}`
|
||||
: `${user.organization.metadata.requestedSlug}.${subdomainSuffix()}`
|
||||
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex rounded-md">
|
||||
|
@ -116,9 +124,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||
ref={usernameRef}
|
||||
name="username"
|
||||
value={inputUsernameValue}
|
||||
addOnLeading={
|
||||
<>{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/</>
|
||||
}
|
||||
addOnLeading={<>{usernamePrefix}/</>}
|
||||
autoComplete="none"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="none"
|
||||
|
|
|
@ -3,6 +3,7 @@ import { collectEvents } from "next-collect/server";
|
|||
import type { NextMiddleware } from "next/server";
|
||||
import { NextResponse, userAgent } from "next/server";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { isIpInBanlist } from "@calcom/lib/getIP";
|
||||
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
||||
|
@ -10,6 +11,15 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
|
|||
const middleware: NextMiddleware = async (req) => {
|
||||
const url = req.nextUrl;
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.get("host") ?? "");
|
||||
|
||||
// Make sure we are in the presence of an organization
|
||||
if (isValidOrgDomain && url.pathname === "/") {
|
||||
// In the presence of an organization, cover its profile page at "/"
|
||||
// rewrites for org profile page using team profile page
|
||||
url.pathname = `/org/${currentOrgDomain}`;
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
|
||||
if (!url.pathname.startsWith("/api")) {
|
||||
//
|
||||
|
@ -70,6 +80,25 @@ const middleware: NextMiddleware = async (req) => {
|
|||
requestHeaders.set("x-csp-enforce", "true");
|
||||
}
|
||||
|
||||
if (isValidOrgDomain) {
|
||||
// Match /:slug to determine if it corresponds to org subteam slug or org user slug
|
||||
const [first, slug, ...rest] = url.pathname.split("/");
|
||||
// In the presence of an organization, if not team profile, a user or team is being accessed
|
||||
if (first === "" && rest.length === 0) {
|
||||
// Fetch the corresponding subteams for the entered organization
|
||||
const getSubteams = await fetch(`${WEBAPP_URL}/api/organizations/${currentOrgDomain}/subteams`);
|
||||
if (getSubteams.ok) {
|
||||
const data = await getSubteams.json();
|
||||
// Treat entered slug as a team if found in the subteams fetched
|
||||
if (data.slugs.includes(slug)) {
|
||||
// Rewriting towards /team/:slug to bring up the team profile within the org
|
||||
url.pathname = `/team/${slug}`;
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
|
@ -79,6 +108,8 @@ const middleware: NextMiddleware = async (req) => {
|
|||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/",
|
||||
"/:path*",
|
||||
"/api/collect-events/:path*",
|
||||
"/api/auth/:path*",
|
||||
"/apps/routing_forms/:path*",
|
||||
|
|
|
@ -170,6 +170,10 @@ const nextConfig = {
|
|||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/org/:slug",
|
||||
destination: "/team/:slug",
|
||||
},
|
||||
{
|
||||
source: "/:user/avatar.png",
|
||||
destination: "/api/user/avatar?username=:user",
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
useEmbedStyles,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
@ -255,6 +256,7 @@ const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
|||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
const crypto = await import("crypto");
|
||||
const { isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||
|
||||
const usernameList = getUsernameList(context.query.user as string);
|
||||
const dataFetchStart = Date.now();
|
||||
|
@ -272,6 +274,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
bio: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
organizationId: true,
|
||||
theme: true,
|
||||
away: true,
|
||||
verified: true,
|
||||
|
@ -284,7 +287,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||
}));
|
||||
|
||||
if (!users.length) {
|
||||
if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { default } from "@calcom/features/ee/organizations/api/subteams";
|
|
@ -34,7 +34,7 @@ type RecurringInfo = {
|
|||
|
||||
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
||||
|
||||
const descriptionByStatus: Record<BookingListingStatus, string> = {
|
||||
const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
|
||||
upcoming: "upcoming_bookings",
|
||||
recurring: "recurring_bookings",
|
||||
past: "past_bookings",
|
||||
|
|
|
@ -9,6 +9,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { Button, StepCard, Steps } from "@calcom/ui";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
@ -202,6 +203,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
darkBrandColor: true,
|
||||
metadata: true,
|
||||
timeFormat: true,
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
allowDynamicBooking: true,
|
||||
defaultScheduleId: true,
|
||||
completedOnboarding: true,
|
||||
|
@ -233,6 +240,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
...(await serverSideTranslations(context.locale ?? "", ["common"])),
|
||||
user: {
|
||||
...user,
|
||||
...(user.organization && {
|
||||
organization: {
|
||||
...user.organization,
|
||||
metadata: teamMetadataSchema.parse(user.organization.metadata),
|
||||
},
|
||||
}),
|
||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||
},
|
||||
hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
|
||||
|
|
|
@ -193,7 +193,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
return {
|
||||
props: {
|
||||
isUnpublished: true,
|
||||
team: unpublishedTeam,
|
||||
team: { ...unpublishedTeam, createdAt: null },
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"awaiting_payment_subject": "Esperant el pagament: {{title}} el {{date}}",
|
||||
"help": "Ajuda",
|
||||
"price": "Preu",
|
||||
"paid": "Pagat",
|
||||
"refunded": "Reembossat",
|
||||
"payment": "Pagament",
|
||||
"missing_card_fields": "Falten camps de la targeta",
|
||||
"pay_now": "Paga ara",
|
||||
"codebase_has_to_stay_opensource": "El codi font ha de seguir com codi obert, tant si s'ha modificat com si no",
|
||||
"cannot_repackage_codebase": "No pots tornar a empaquetar ni vendre el codi font",
|
||||
"acquire_license": "Adquireix una llicència comercial per eliminar aquests termes mitjançant un correu electrònic",
|
||||
"terms_summary": "Resum dels termes",
|
||||
"open_env": "Obre .env i accepta la nostra llicència",
|
||||
"env_changed": "He canviat el meu .env",
|
||||
"accept_license": "Accepta la llicència",
|
||||
"still_waiting_for_approval": "Un esdeveniment encara està pendent d'aprovació",
|
||||
"event_is_still_waiting": "La sol·licitud d'esdeveniment encara està pendent: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"no_more_results": "No hi ha més resultats",
|
||||
"no_results": "No hi ha resultats",
|
||||
"load_more_results": "Carrega més resultats",
|
||||
"integration_meeting_id": "{{integrationName}} Identificador de la reunió: {{meetingId}}",
|
||||
"confirmed_event_type_subject": "Confirmat: {{eventType}} amb {{name}} el {{date}}"
|
||||
}
|
|
@ -11,7 +11,16 @@
|
|||
"calcom_explained_new_user": "Završite podešavanje vašeg {{appName}} naloga! Na par koraka ste od rešavanja svih problema sa rasporedom.",
|
||||
"have_any_questions": "Imate pitanja? Tu smo da pomognemo.",
|
||||
"reset_password_subject": "{{appName}}: Upustva za resetovanje lozinke",
|
||||
"verify_email_subject": "{{appName}}: Potvrdi svoj nalog",
|
||||
"check_your_email": "Proveri imejl",
|
||||
"verify_email_page_body": "Poslali smo imejl na {{email}}. Važno je da potvrdite svoju imejl adresu da biste obezbedili najbolju isporuku imejla i kalendara od aplikacije {{appName}}.",
|
||||
"verify_email_banner_body": "Potvrdite svoju imejl adresu da biste obezbedili najbolju isporuku imejla i kalendara od aplikacije {{appName}}.",
|
||||
"verify_email_banner_button": "Pošaljite imejl",
|
||||
"verify_email_email_header": "Potvrdite svoju imejl adresu",
|
||||
"verify_email_email_button": "Potvrdi imejl",
|
||||
"verify_email_email_body": "Potvrdite svoju imejl adresu klikom na dugme ispod.",
|
||||
"verify_email_email_link_text": "U slučaju da ne volite da klikćete na dugmad, evo linka:",
|
||||
"email_sent": "Imejl je uspešno poslat",
|
||||
"event_declined_subject": "Odbijeno: {{title}} datuma {{date}}",
|
||||
"event_cancelled_subject": "Otkazano: {{title}} datuma {{date}}",
|
||||
"event_request_declined": "Vaš zahtev za događaj je odbijen",
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const querySchema = z.object({
|
||||
org: z.string({ required_error: "org slug is required" }),
|
||||
});
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const parsedQuery = querySchema.safeParse(req.query);
|
||||
|
||||
if (!parsedQuery.success) throw new HttpError({ statusCode: 400, message: parsedQuery.error.message });
|
||||
|
||||
const {
|
||||
data: { org: slug },
|
||||
} = parsedQuery;
|
||||
if (!slug) if (!slug) return res.status(400).json({ message: "Org is needed" });
|
||||
|
||||
const org = await prisma.team.findFirst({ where: { slug }, select: { children: true, metadata: true } });
|
||||
|
||||
if (!org) return res.status(400).json({ message: "Org doesn't exist" });
|
||||
|
||||
const metadata = teamMetadataSchema.parse(org?.metadata);
|
||||
|
||||
if (!metadata?.isOrganization) return res.status(400).json({ message: "Team is not an org" });
|
||||
|
||||
return res.status(200).json({ slugs: org.children.map((ch) => ch.slug) });
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
|
@ -5,6 +5,7 @@ import { useState } from "react";
|
|||
import useDigitInput from "react-digit-input";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
|
@ -77,6 +78,7 @@ export const VerifyCodeDialog = ({
|
|||
open={isOpenDialog}
|
||||
onOpenChange={(open) => {
|
||||
onChange("");
|
||||
setError("");
|
||||
setIsOpenDialog(open);
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
|
@ -268,10 +270,7 @@ export const CreateANewOrganizationForm = () => {
|
|||
name="slug"
|
||||
label={t("organization_url")}
|
||||
placeholder="acme"
|
||||
addOnSuffix={`.${process.env.NEXT_PUBLIC_WEBSITE_URL?.replace("https://", "")?.replace(
|
||||
"http://",
|
||||
""
|
||||
)}`}
|
||||
addOnSuffix={`.${subdomainSuffix()}`}
|
||||
defaultValue={value}
|
||||
onChange={(e) => {
|
||||
newOrganizationFormMethods.setValue("slug", slugify(e?.target.value), {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
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"];
|
||||
|
||||
export function getOrgDomain(hostname: string) {
|
||||
// Find which hostname is being currently used
|
||||
const currentHostname = appHostnames.find((ahn) => {
|
||||
const url = new URL(WEBAPP_URL);
|
||||
const hostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`;
|
||||
return hostname.endsWith(`.${ahn}`);
|
||||
});
|
||||
// Define which is the current domain/subdomain
|
||||
return hostname.replace(`.${currentHostname}` ?? "", "");
|
||||
}
|
||||
|
||||
export function orgDomainConfig(hostname: string) {
|
||||
const currentOrgDomain = getOrgDomain(hostname);
|
||||
return {
|
||||
currentOrgDomain,
|
||||
isValidOrgDomain: currentOrgDomain !== "app" && !appHostnames.includes(currentOrgDomain),
|
||||
};
|
||||
}
|
||||
|
||||
export function subdomainSuffix() {
|
||||
const urlSplit = WEBAPP_URL.replace("https://", "")?.replace("http://", "").split(".");
|
||||
return urlSplit.length === 3 ? urlSplit.slice(1).join(".") : urlSplit.join(".");
|
||||
}
|
|
@ -131,7 +131,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
</div>
|
||||
|
||||
<Form form={newMemberFormMethods} handleSubmit={(values) => props.onSubmit(values)}>
|
||||
<div className="space-y-6">
|
||||
<div className="my-6 space-y-6">
|
||||
{/* Indivdual Invite */}
|
||||
{modalImportMode === "INDIVIDUAL" && (
|
||||
<Controller
|
||||
|
@ -240,9 +240,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter showDivider>
|
||||
<div className="mr-auto flex">
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
|
@ -271,6 +269,8 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter showDivider>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
|
|
|
@ -293,7 +293,7 @@ export default function CreateEventTypeDialog({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-8 flex justify-end gap-x-2">
|
||||
<div className="mt-10 flex justify-end gap-x-2">
|
||||
<DialogClose />
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
|
|
|
@ -425,7 +425,7 @@ export const FormBuilder = function FormBuilder({
|
|||
})
|
||||
}>
|
||||
<DialogContent className="max-h-none p-0" data-testid="edit-field-dialog">
|
||||
<div className="h-auto max-h-[85vh] overflow-auto px-8 pt-8 pb-7">
|
||||
<div className="h-auto max-h-[85vh] overflow-auto px-8 pt-8 pb-10">
|
||||
<DialogHeader
|
||||
title={t("add_a_booking_question")}
|
||||
subtitle={t("form_builder_field_add_subtitle")}
|
||||
|
@ -545,7 +545,7 @@ export const FormBuilder = function FormBuilder({
|
|||
/>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter className="relative rounded px-8 pb-6" showDivider>
|
||||
<DialogFooter className="relative rounded px-8 pb-4" showDivider>
|
||||
<DialogClose color="secondary">{t("cancel")}</DialogClose>
|
||||
<Button data-testid="field-add-save" type="submit" form="form-builder">
|
||||
{isFieldEditMode ? t("save") : t("add")}
|
||||
|
|
|
@ -342,11 +342,11 @@ function UserDropdown({ small }: { small?: boolean }) {
|
|||
</span>
|
||||
{!small && (
|
||||
<span className="flex flex-grow items-center truncate">
|
||||
<span className="flex-grow truncate text-sm">
|
||||
<span className="text-emphasis mb-1 block truncate pb-1 font-medium leading-none">
|
||||
<span className="flex-grow truncate text-sm leading-none">
|
||||
<span className="text-emphasis mb-1 block truncate font-medium">
|
||||
{user.name || "Nameless User"}
|
||||
</span>
|
||||
<span className="text-default truncate pb-1 font-normal leading-none">
|
||||
<span className="text-default truncate pb-1 font-normal">
|
||||
{user.username
|
||||
? process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com"
|
||||
? `cal.com/${user.username}`
|
||||
|
|
|
@ -8,7 +8,7 @@ export const WEBAPP_URL =
|
|||
RAILWAY_STATIC_URL ||
|
||||
HEROKU_URL ||
|
||||
RENDER_URL ||
|
||||
"http://localhost:3000";
|
||||
"http://app.cal.local:3000";
|
||||
/** @deprecated use `WEBAPP_URL` */
|
||||
export const BASE_URL = WEBAPP_URL;
|
||||
export const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL || "https://cal.com";
|
||||
|
|
|
@ -20,7 +20,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
|
|||
const cookiePrefix = useSecureCookies ? "__Secure-" : "";
|
||||
|
||||
const defaultOptions: CookieOption["options"] = {
|
||||
domain: isENVDev ? undefined : NEXTAUTH_COOKIE_DOMAIN,
|
||||
domain: isENVDev ? "cal.local" : NEXTAUTH_COOKIE_DOMAIN,
|
||||
// To enable cookies on widgets,
|
||||
// https://stackoverflow.com/questions/45094712/iframe-not-reading-cookies-in-chrome
|
||||
// But we need to set it as `lax` in development
|
||||
|
|
|
@ -3,7 +3,8 @@ import { totp } from "otplib";
|
|||
|
||||
import { sendOrganizationEmailVerification } from "@calcom/emails";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { IS_PRODUCTION, IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
@ -20,6 +21,32 @@ type CreateOptions = {
|
|||
input: TCreateInputSchema;
|
||||
};
|
||||
|
||||
const vercelCreateDomain = async (domain: string) => {
|
||||
const response = await fetch(
|
||||
`https://api.vercel.com/v8/projects/${process.env.PROJECT_ID_VERCEL}/domains?teamId=${process.env.TEAM_ID_VERCEL}`,
|
||||
{
|
||||
body: `{\n "name": "${domain}.${subdomainSuffix()}"\n}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Domain is already owned by another team but you can request delegation to access it
|
||||
if (data.error?.code === "forbidden")
|
||||
throw new TRPCError({ code: "CONFLICT", message: "domain_taken_team" });
|
||||
|
||||
// Domain is already being used by a different project
|
||||
if (data.error?.code === "domain_taken")
|
||||
throw new TRPCError({ code: "CONFLICT", message: "domain_taken_project" });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const createHandler = async ({ input }: CreateOptions) => {
|
||||
const { slug, name, adminEmail, adminUsername, check } = input;
|
||||
|
||||
|
@ -67,6 +94,8 @@ export const createHandler = async ({ input }: CreateOptions) => {
|
|||
},
|
||||
});
|
||||
|
||||
if (IS_PRODUCTION) await vercelCreateDomain(slug);
|
||||
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
userId: createOwnerOrg.id,
|
||||
|
|
|
@ -138,7 +138,7 @@ export function DialogFooter(props: { children: ReactNode; className?: string; s
|
|||
return (
|
||||
<div className={classNames("bg-default", props.className)}>
|
||||
{props.showDivider && <hr className="border-subtle absolute right-0 w-full" />}
|
||||
<div className={classNames("bg-default mt-6 flex justify-end space-x-2 rtl:space-x-reverse")}>
|
||||
<div className={classNames("-mb-4 flex justify-end space-x-2 pt-4 rtl:space-x-reverse")}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -276,6 +276,9 @@
|
|||
"ZOHOCRM_CLIENT_SECRET",
|
||||
"ZOOM_CLIENT_ID",
|
||||
"ZOOM_CLIENT_SECRET",
|
||||
"INTERCOM_SECRET"
|
||||
"INTERCOM_SECRET",
|
||||
"PROJECT_ID_VERCEL",
|
||||
"TEAM_ID_VERCEL",
|
||||
"AUTH_BEARER_TOKEN_VERCEL"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user