Files
calendar/packages/lib/domainManager/deploymentServices/vercel.ts
T
837349e3d3 feat: Organization onboarding improvement - Payment before creation of org but onboarding doesn't require payment (#18990)
* create seed for org upgrade

* migrate about/new pages

* pages refactor + use persistant zustand store

* invited members view + confirm button

* add check to org slug

* check reserved subdomain

* remove quotes from subdomain env var

* intro to creation + billing

* create price

* fix types

* createWithPaymentIntent + permission check on teams

* open stripe link in popup

* intro to tests

* organization price and seat override tests

* move permissions to a new permission service

* update env and permission check

* intro to paid invoice trigger

* dont use subId

* wip

* Get e2e working with migration of teams and members

* fix ts errors

* Get flow working again

* Fix various issues and refactor

* Some more fixes

* Fix wrong page route link

* Platform onboarding fix and moving members of team to org rix

* Platform onboarding fix and moving members of team to org rix

* Fix tests, found a bug

* Get custom price flow working

* Get admin impersonation flow working for a non-existent user

* Fix unit test

* Admin onboarding handover

* fix admin onboarding tests

* fix ts error

* Get updateQuantity working

* More fixes

* fix test

* fix schema name

* Add tests

* Add missing file

* More tests

* Add more tests

* fix mt-2 moving down input into overflow

* fix handover layout removing HOC

* Update PR_TODO.md

* fix ts error due to merge conflict

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-03-13 11:17:43 +00:00

149 lines
4.1 KiB
TypeScript

import z from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { safeStringify } from "@calcom/lib/safeStringify";
import logger from "../../logger";
const log = logger.getSubLogger({ prefix: ["Vercel/DomainManager"] });
const vercelApiForProjectUrl = `https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}`;
const vercelDomainApiResponseSchema = z.object({
error: z
.object({
code: z.string().nullish(),
domain: z.string().nullish(),
})
.optional(),
});
export const createDomain = async (domain: string) => {
assertVercelEnvVars(process.env);
log.info(`Creating domain in Vercel: ${domain}`);
const response = await fetch(`${vercelApiForProjectUrl}/domains?teamId=${process.env.TEAM_ID_VERCEL}`, {
body: JSON.stringify({ name: domain }),
headers: {
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
"Content-Type": "application/json",
},
method: "POST",
});
const data = vercelDomainApiResponseSchema.parse(await response.json());
if (!data.error) {
return true;
}
return handleDomainCreationError(data.error);
};
export const deleteDomain = async (domain: string) => {
log.info(`Deleting domain in Vercel: ${domain}`);
assertVercelEnvVars(process.env);
const response = await fetch(
`${vercelApiForProjectUrl}/domains/${domain}?teamId=${process.env.TEAM_ID_VERCEL}`,
{
headers: {
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
},
method: "DELETE",
}
);
const data = vercelDomainApiResponseSchema.parse(await response.json());
if (!data.error) {
return true;
}
return handleDomainDeletionError(data.error);
};
function handleDomainCreationError(error: { code?: string | null; domain?: string | null }) {
// Domain is already owned by another team but you can request delegation to access it
if (error.code === "forbidden") {
const errorMessage = "Domain is already owned by another team";
log.error(
safeStringify({
errorMessage,
vercelError: error,
})
);
throw new HttpError({
message: errorMessage,
statusCode: 400,
});
}
if (error.code === "domain_taken") {
const errorMessage = "Domain is already being used by a different project";
log.error(
safeStringify({
errorMessage,
vercelError: error,
})
);
throw new HttpError({
message: errorMessage,
statusCode: 400,
});
}
if (error.code === "domain_already_in_use") {
// Domain is already configured correctly, this is not an error when it happens during creation as it could be re-attempt to create an existing domain
return true;
}
const errorMessage = `Failed to create domain on Vercel: ${error.domain}`;
log.error(safeStringify({ errorMessage, vercelError: error }));
throw new HttpError({
message: errorMessage,
statusCode: 400,
});
}
function handleDomainDeletionError(error: { code?: string | null; domain?: string | null }) {
if (error.code === "not_found") {
// Domain is already deleted
return true;
}
// Domain is already owned by another team but you can request delegation to access it
if (error.code === "forbidden") {
const errorMessage = "Domain is owned by another team";
log.error(
safeStringify({
errorMessage,
vercelError: error,
})
);
throw new HttpError({
message: errorMessage,
statusCode: 400,
});
}
const errorMessage = `Failed to take action for domain: ${error.domain}`;
log.error(safeStringify({ errorMessage, vercelError: error }));
throw new HttpError({
message: errorMessage,
statusCode: 400,
});
}
function assertVercelEnvVars(env: typeof process.env): asserts env is {
PROJECT_ID_VERCEL: string;
TEAM_ID_VERCEL: string;
AUTH_BEARER_TOKEN_VERCEL: string;
} & typeof process.env {
if (!env.PROJECT_ID_VERCEL) {
throw new Error("Missing env var: PROJECT_ID_VERCEL");
}
// TEAM_ID_VERCEL is optional
if (!env.AUTH_BEARER_TOKEN_VERCEL) {
throw new Error("Missing env var: AUTH_BEARER_TOKEN_VERCEL");
}
}