From bd10eb99834453acc24edbf8b119f297286346f1 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Fri, 16 Jan 2026 18:45:48 +0530 Subject: [PATCH] feat: seed admin API key for local development (#26876) * feat: seed admin API key for local development Co-Authored-By: hariom@cal.com * refactor: query API key by hash instead of separate ID Co-Authored-By: hariom@cal.com * refactor: remove redundant comments from seedApiKey Co-Authored-By: hariom@cal.com * refactor: make seedApiKey reusable and use owner1-acme user Co-Authored-By: hariom@cal.com * refactor: extract ensureAcmeOwnerHasApiKeySeeded wrapper function Co-Authored-By: hariom@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- scripts/seed.ts | 227 +++++++++++++++++------------------------------- 1 file changed, 79 insertions(+), 148 deletions(-) diff --git a/scripts/seed.ts b/scripts/seed.ts index f53e49d47e..524b6090cc 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -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) - ?.requestedSlug; + const requestedSlug = (team.metadata as z.infer)?.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, - "username" | "name" | "email" | "password" - >; + memberData: Ensure, "username" | "name" | "email" | "password">; orgMembership: Partial; orgProfile: { username: string; @@ -337,14 +310,8 @@ async function createOrganizationAndAddMembersAndTeams({ }[]; }; teams: { - teamData: Omit< - Ensure, "name" | "slug">, - "members" - >; - nonOrgMembers: Ensure< - Partial, - "username" | "name" | "email" | "password" - >[]; + teamData: Omit, "name" | "slug">, "members">; + nonOrgMembers: Ensure, "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() {