feat: seed admin API key for local development (#26876)

* feat: seed admin API key for local development

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: query API key by hash instead of separate ID

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: remove redundant comments from seedApiKey

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: make seedApiKey reusable and use owner1-acme user

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: extract ensureAcmeOwnerHasApiKeySeeded wrapper function

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Hariom Balhara
2026-01-16 18:45:48 +05:30
committed by GitHub
co-authored by Devin AI
parent 7a9fdd5a0c
commit bd10eb9983
+79 -148
View File
@@ -1,32 +1,19 @@
import { uuid } from "short-uuid";
import type z from "zod";
import process from "node:process";
import dailyMeta from "@calcom/app-store/dailyvideo/_metadata";
import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
import dayjs from "@calcom/dayjs";
import { hashAPIKey } from "@calcom/ee/api-keys/lib/apiKeys";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { hashPassword } from "@calcom/lib/auth/hashPassword";
import {
DEFAULT_SCHEDULE,
getAvailabilityFromSchedule,
} from "@calcom/lib/availability";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { prisma } from "@calcom/prisma";
import type {
Membership,
Team,
User,
UserPermissionRole,
} from "@calcom/prisma/client";
import type { Membership, Team, User, UserPermissionRole } from "@calcom/prisma/client";
import { Prisma } from "@calcom/prisma/client";
import {
BookingStatus,
MembershipRole,
RedirectType,
SchedulingType,
} from "@calcom/prisma/enums";
import { BookingStatus, MembershipRole, RedirectType, SchedulingType } from "@calcom/prisma/enums";
import type { Ensure } from "@calcom/types/utils";
import { uuid } from "short-uuid";
import type z from "zod";
import type { teamMetadataSchema } from "../packages/prisma/zod-utils";
import mainAppStore from "./seed-app-store";
import mainHugeEventTypesSeed from "./seed-huge-event-types";
@@ -70,7 +57,7 @@ const setupPlatformUser = async (user: PlatformUser) => {
completedOnboarding: user.completedOnboarding ?? true,
locale: "en",
schedules:
user.completedOnboarding ?? true
(user.completedOnboarding ?? true)
? {
create: {
name: "Working Hours",
@@ -110,8 +97,7 @@ const setupPlatformUser = async (user: PlatformUser) => {
const createTeam = async (team: Prisma.TeamCreateInput) => {
try {
const requestedSlug = (team.metadata as z.infer<typeof teamMetadataSchema>)
?.requestedSlug;
const requestedSlug = (team.metadata as z.infer<typeof teamMetadataSchema>)?.requestedSlug;
if (requestedSlug) {
const unpublishedTeam = await checkUnpublishedTeam(requestedSlug);
if (unpublishedTeam) {
@@ -124,10 +110,7 @@ const createTeam = async (team: Prisma.TeamCreateInput) => {
},
});
} catch (_err) {
if (
_err instanceof Error &&
_err.message.indexOf("Unique constraint failed on the fields") !== -1
) {
if (_err instanceof Error && _err.message.indexOf("Unique constraint failed on the fields") !== -1) {
console.log(`Team '${team.name}' already exists, skipping.`);
return;
}
@@ -135,12 +118,7 @@ const createTeam = async (team: Prisma.TeamCreateInput) => {
}
};
const associateUserAndOrg = async ({
teamId,
userId,
role,
username,
}: AssociateUserAndOrgProps) => {
const associateUserAndOrg = async ({ teamId, userId, role, username }: AssociateUserAndOrgProps) => {
await prisma.membership.create({
data: {
createdAt: new Date(),
@@ -189,7 +167,7 @@ async function createPlatformAndSetupUser({
const membershipRole = MembershipRole.OWNER;
if (!!team) {
if (team) {
await associateUserAndOrg({
teamId: team.id,
userId: platformUser.id,
@@ -222,9 +200,7 @@ async function createPlatformAndSetupUser({
},
});
}
console.log(
`\t👤 Added '${teamInput.name}' membership for '${username}' with role '${membershipRole}'`
);
console.log(`\t👤 Added '${teamInput.name}' membership for '${username}' with role '${membershipRole}'`);
}
}
@@ -325,10 +301,7 @@ async function createOrganizationAndAddMembersAndTeams({
organizationSettings: Prisma.OrganizationSettingsCreateWithoutOrganizationInput;
};
members: {
memberData: Ensure<
Partial<Prisma.UserCreateInput>,
"username" | "name" | "email" | "password"
>;
memberData: Ensure<Partial<Prisma.UserCreateInput>, "username" | "name" | "email" | "password">;
orgMembership: Partial<Membership>;
orgProfile: {
username: string;
@@ -337,14 +310,8 @@ async function createOrganizationAndAddMembersAndTeams({
}[];
};
teams: {
teamData: Omit<
Ensure<Partial<Prisma.TeamCreateInput>, "name" | "slug">,
"members"
>;
nonOrgMembers: Ensure<
Partial<Prisma.UserCreateInput>,
"username" | "name" | "email" | "password"
>[];
teamData: Omit<Ensure<Partial<Prisma.TeamCreateInput>, "name" | "slug">, "members">;
nonOrgMembers: Ensure<Partial<Prisma.UserCreateInput>, "username" | "name" | "email" | "password">[];
}[];
usersOutsideOrg: {
name: string;
@@ -362,9 +329,7 @@ async function createOrganizationAndAddMembersAndTeams({
});
if (existingTeam) {
console.log(
`Organization with slug '${orgData.slug}' already exists, skipping.`
);
console.log(`Organization with slug '${orgData.slug}' already exists, skipping.`);
return;
}
@@ -388,8 +353,7 @@ async function createOrganizationAndAddMembersAndTeams({
user: {
...member.memberData,
theme:
member.memberData.theme === "dark" ||
member.memberData.theme === "light"
member.memberData.theme === "dark" || member.memberData.theme === "light"
? member.memberData.theme
: undefined,
password: member.memberData.password.create?.hash ?? "",
@@ -428,17 +392,13 @@ async function createOrganizationAndAddMembersAndTeams({
},
},
update: {
toUrl: `${getOrgFullOrigin(orgData.slug)}/${
member.orgProfile.username
}`,
toUrl: `${getOrgFullOrigin(orgData.slug)}/${member.orgProfile.username}`,
},
create: {
fromOrgId: 0,
type: RedirectType.User,
from: member.memberData.username,
toUrl: `${getOrgFullOrigin(orgData.slug)}/${
member.orgProfile.username
}`,
toUrl: `${getOrgFullOrigin(orgData.slug)}/${member.orgProfile.username}`,
},
});
@@ -451,9 +411,7 @@ async function createOrganizationAndAddMembersAndTeams({
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
console.log(
`One of the organization members already exists, skipping the entire seeding`
);
console.log(`One of the organization members already exists, skipping the entire seeding`);
return;
}
}
@@ -485,9 +443,7 @@ async function createOrganizationAndAddMembersAndTeams({
data: {
...restOrgData,
metadata: {
...(orgData.metadata && typeof orgData.metadata === "object"
? orgData.metadata
: {}),
...(orgData.metadata && typeof orgData.metadata === "object" ? orgData.metadata : {}),
isOrganization: true,
},
organizationSettings: {
@@ -685,7 +641,7 @@ async function createOrganizationAndAddMembersAndTeams({
// Create memberships for all the organization members with the respective teams
for (const member of orgMembersInDBWithProfileId) {
for (const { slug: teamSlug, role: role } of member.inTeams) {
for (const { slug: teamSlug, role } of member.inTeams) {
const team = organizationTeams.find((t) => t.slug === teamSlug);
if (!team) {
throw Error(`Team with slug ${teamSlug} not found`);
@@ -703,6 +659,40 @@ async function createOrganizationAndAddMembersAndTeams({
}
}
async function seedApiKey(userId: number, apiKey: string) {
const hashedKey = hashAPIKey(apiKey);
const existingKey = await prisma.apiKey.findFirst({
where: { hashedKey },
});
if (existingKey) {
console.log(`🔑 API Key already exists, skipping.`);
return;
}
await prisma.apiKey.create({
data: {
userId,
hashedKey,
note: "Seeded API Key for local development",
expiresAt: null,
},
});
const apiKeyPrefix = process.env.API_KEY_PREFIX ?? "cal_";
console.log(`🔑 Created seeded API Key: ${apiKeyPrefix}${apiKey}`);
}
async function ensureAcmeOwnerHasApiKeySeeded() {
const owner1AcmeUser = await prisma.user.findFirst({
where: { email: "owner1-acme@example.com" },
});
if (owner1AcmeUser) {
await seedApiKey(owner1AcmeUser.id, "0123456789abcdef0123456789abcdef");
}
}
async function main() {
await createUserAndEventType({
user: {
@@ -844,11 +834,7 @@ async function main() {
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(1, "week").toDate(),
endTime: dayjs()
.add(1, "day")
.add(1, "week")
.add(30, "minutes")
.toDate(),
endTime: dayjs().add(1, "day").add(1, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
@@ -856,11 +842,7 @@ async function main() {
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(2, "week").toDate(),
endTime: dayjs()
.add(1, "day")
.add(2, "week")
.add(30, "minutes")
.toDate(),
endTime: dayjs().add(1, "day").add(2, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
@@ -868,11 +850,7 @@ async function main() {
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(3, "week").toDate(),
endTime: dayjs()
.add(1, "day")
.add(3, "week")
.add(30, "minutes")
.toDate(),
endTime: dayjs().add(1, "day").add(3, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
@@ -880,11 +858,7 @@ async function main() {
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(4, "week").toDate(),
endTime: dayjs()
.add(1, "day")
.add(4, "week")
.add(30, "minutes")
.toDate(),
endTime: dayjs().add(1, "day").add(4, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
@@ -892,19 +866,14 @@ async function main() {
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(5, "week").toDate(),
endTime: dayjs()
.add(1, "day")
.add(5, "week")
.add(30, "minutes")
.toDate(),
endTime: dayjs().add(1, "day").add(5, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId:
Buffer.from("seeded-yoga-class").toString("base64"),
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").toDate(),
endTime: dayjs().subtract(4, "day").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
@@ -913,42 +882,27 @@ async function main() {
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId:
Buffer.from("seeded-yoga-class").toString("base64"),
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").add(1, "week").toDate(),
endTime: dayjs()
.subtract(4, "day")
.add(1, "week")
.add(30, "minutes")
.toDate(),
endTime: dayjs().subtract(4, "day").add(1, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId:
Buffer.from("seeded-yoga-class").toString("base64"),
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").add(2, "week").toDate(),
endTime: dayjs()
.subtract(4, "day")
.add(2, "week")
.add(30, "minutes")
.toDate(),
endTime: dayjs().subtract(4, "day").add(2, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId:
Buffer.from("seeded-yoga-class").toString("base64"),
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").add(3, "week").toDate(),
endTime: dayjs()
.subtract(4, "day")
.add(3, "week")
.add(30, "minutes")
.toDate(),
endTime: dayjs().subtract(4, "day").add(3, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
],
@@ -973,11 +927,7 @@ async function main() {
title: "Tennis class",
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(2, "week").toDate(),
endTime: dayjs()
.add(2, "day")
.add(2, "week")
.add(60, "minutes")
.toDate(),
endTime: dayjs().add(2, "day").add(2, "week").add(60, "minutes").toDate(),
status: BookingStatus.PENDING,
},
{
@@ -985,11 +935,7 @@ async function main() {
title: "Tennis class",
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(4, "week").toDate(),
endTime: dayjs()
.add(2, "day")
.add(4, "week")
.add(60, "minutes")
.toDate(),
endTime: dayjs().add(2, "day").add(4, "week").add(60, "minutes").toDate(),
status: BookingStatus.PENDING,
},
{
@@ -997,11 +943,7 @@ async function main() {
title: "Tennis class",
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(8, "week").toDate(),
endTime: dayjs()
.add(2, "day")
.add(8, "week")
.add(60, "minutes")
.toDate(),
endTime: dayjs().add(2, "day").add(8, "week").add(60, "minutes").toDate(),
status: BookingStatus.PENDING,
},
{
@@ -1009,11 +951,7 @@ async function main() {
title: "Tennis class",
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(10, "week").toDate(),
endTime: dayjs()
.add(2, "day")
.add(10, "week")
.add(60, "minutes")
.toDate(),
endTime: dayjs().add(2, "day").add(10, "week").add(60, "minutes").toDate(),
status: BookingStatus.PENDING,
},
],
@@ -1172,12 +1110,7 @@ async function main() {
},
});
if (
!!(
process.env.E2E_TEST_CALCOM_QA_EMAIL &&
process.env.E2E_TEST_CALCOM_QA_PASSWORD
)
) {
if (process.env.E2E_TEST_CALCOM_QA_EMAIL && process.env.E2E_TEST_CALCOM_QA_PASSWORD) {
await createUserAndEventType({
user: {
email: process.env.E2E_TEST_CALCOM_QA_EMAIL || "qa@example.com",
@@ -1193,12 +1126,10 @@ async function main() {
},
],
credentials: [
!!process.env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS
process.env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS
? {
type: "google_calendar",
key: JSON.parse(
process.env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS
) as Prisma.JsonObject,
key: JSON.parse(process.env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS) as Prisma.JsonObject,
appId: "google-calendar",
}
: null,
@@ -1531,9 +1462,7 @@ async function main() {
},
});
if (form) {
console.log(
`Skipping Routing Form - Form Seed, "Seeded Form - Pro" already exists`
);
console.log(`Skipping Routing Form - Form Seed, "Seeded Form - Pro" already exists`);
} else {
const proUser = await prisma.user.findFirst({
where: {
@@ -1691,6 +1620,8 @@ async function main() {
});
}
}
await ensureAcmeOwnerHasApiKeySeeded();
}
async function runSeed() {