feat: (PBAC) Add db seed (#22704)

* add a seed for pbac

* remove readme

* add team feature

* add seed script to json

* log seed detials
This commit is contained in:
sean-brydon
2025-07-24 22:05:44 +00:00
committed by GitHub
parent 3228d8d869
commit 045ea34580
5 changed files with 878 additions and 3 deletions
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env tsx
/**
* Cleanup script to remove PBAC demo organization and related data
* Run with: npx tsx packages/prisma/cleanup-pbac.ts
*/
import prisma from ".";
async function cleanupPBACOrganization() {
console.log("🧹 Cleaning up PBAC Demo Organization...");
try {
// Find the organization
const organization = await prisma.team.findFirst({
where: {
slug: "pbac-demo-org",
isOrganization: true,
},
include: {
members: true,
roles: true,
children: true, // teams within the organization
},
});
if (!organization) {
console.log("️ PBAC Demo Organization not found, nothing to clean up");
return;
}
console.log(`Found organization: ${organization.name} (ID: ${organization.id})`);
// Delete users created for PBAC demo
const pbacUserEmails = [
"owner@pbac-demo.com",
"events@pbac-demo.com",
"analytics@pbac-demo.com",
"coordinator@pbac-demo.com",
"support@pbac-demo.com",
];
const pbacUsers = await prisma.user.findMany({
where: {
email: { in: pbacUserEmails },
},
});
console.log(`Found ${pbacUsers.length} PBAC users to delete`);
// Delete users (this will cascade delete memberships, profiles, etc.)
for (const user of pbacUsers) {
await prisma.user.delete({
where: { id: user.id },
});
console.log(` ✅ Deleted user: ${user.email}`);
}
// Delete custom roles (this will cascade delete permissions)
for (const role of organization.roles) {
await prisma.role.delete({
where: { id: role.id },
});
console.log(` ✅ Deleted role: ${role.name}`);
}
// Delete child teams
for (const team of organization.children) {
await prisma.team.delete({
where: { id: team.id },
});
console.log(` ✅ Deleted team: ${team.name}`);
}
// Delete the organization (this will cascade delete organization settings)
await prisma.team.delete({
where: { id: organization.id },
});
console.log(` ✅ Deleted organization: ${organization.name}`);
// Clean up any temp org redirects
await prisma.tempOrgRedirect.deleteMany({
where: {
OR: [
{ from: { in: ["owner", "events", "analytics", "coordinator", "support"] } },
{ toUrl: { contains: "pbac-demo-org" } },
],
},
});
console.log(" ✅ Cleaned up temp org redirects");
console.log("\n🎉 PBAC Demo Organization cleanup completed successfully!");
} catch (error) {
console.error("❌ Error during cleanup:", error);
throw error;
} finally {
await prisma.$disconnect();
}
}
cleanupPBACOrganization()
.then(() => {
console.log("✅ Cleanup completed!");
process.exit(0);
})
.catch((error) => {
console.error("❌ Cleanup failed:", error);
process.exit(1);
});
+2 -1
View File
@@ -18,7 +18,8 @@
"post-install": "yarn generate-schemas",
"seed-app-store": "ts-node --transpile-only ./seed-app-store.ts",
"delete-app": "ts-node --transpile-only ./delete-app.ts",
"seed-insights": "ts-node --transpile-only ./seed-insights.ts"
"seed-insights": "ts-node --transpile-only ./seed-insights.ts",
"seed-pbac": "ts-node --transpile-only ./seed-pbac-organization.ts"
},
"devDependencies": {
"@faker-js/faker": "9.2.0",
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env tsx
/**
* Standalone script to create a PBAC-enabled organization with custom roles
* Run with: npx tsx packages/prisma/seed-pbac-only.ts
*/
import { createPBACOrganization } from "./seed-pbac-organization";
async function main() {
console.log("🚀 Starting PBAC organization seed...");
try {
const result = await createPBACOrganization();
console.log("\n🎉 PBAC Organization created successfully!");
console.log("\n📋 Summary:");
console.log(`Organization: ${result.organization.name} (${result.organization.slug})`);
console.log(`Custom Roles: ${Object.keys(result.customRoles).length}`);
console.log(`Users: ${result.users.length}`);
console.log(`Team: ${result.team?.name || "None"} (${result.team?.slug || "N/A"})`);
console.log("\n🔐 Login Credentials:");
result.users.forEach(({ user, role, customRole }) => {
const roleText = customRole ? `${role} + ${customRole}` : role;
console.log(` - ${user.name}: ${user.email} / password (${roleText})`);
});
console.log(`\n🌐 Access URLs:`);
console.log(`Organization: ${process.env.NEXT_PUBLIC_WEBAPP_URL}/org/${result.organization.slug}`);
if (result.team) {
console.log(`Team: ${process.env.NEXT_PUBLIC_WEBAPP_URL}/team/${result.team.slug}`);
}
} catch (error) {
console.error("❌ Error creating PBAC organization:", error);
process.exit(1);
}
}
main()
.then(() => {
console.log("\n✅ Seed completed successfully!");
process.exit(0);
})
.catch((error) => {
console.error("❌ Seed failed:", error);
process.exit(1);
});
+706
View File
@@ -0,0 +1,706 @@
import { uuid } from "short-uuid";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { MembershipRole, RoleType } from "@calcom/prisma/enums";
import prisma from ".";
/**
* Creates an organization with custom roles and PBAC (Permission-Based Access Control) enabled
* This demonstrates how to set up fine-grained permissions for team members
*/
export async function createPBACOrganization() {
console.log("🏢 Creating PBAC-enabled organization with custom roles...");
// Check if organization already exists
const existingOrg = await prisma.team.findFirst({
where: {
slug: "pbac-demo-org",
isOrganization: true,
},
});
if (existingOrg) {
console.log("⚠️ PBAC Demo Organization already exists, skipping creation");
return {
organization: existingOrg,
customRoles: {},
users: [],
team: null,
};
}
// First, create the organization
const organization = await prisma.team.create({
data: {
name: "PBAC Demo Organization",
slug: "pbac-demo-org",
isOrganization: true,
metadata: {
isOrganization: true,
},
organizationSettings: {
create: {
isOrganizationVerified: true,
orgAutoAcceptEmail: "pbac-demo.com",
isAdminAPIEnabled: true,
isAdminReviewed: true,
},
},
},
});
// Add the feature flag
await prisma.teamFeatures.create({
data: {
featureId: "pbac",
teamId: organization.id,
assignedBy: "system (Seed script)",
assignedAt: new Date(),
},
});
console.log(`✅ Created organization: ${organization.name} (ID: ${organization.id})`);
// Create custom roles with specific permissions
const customRoles = await createCustomRoles(organization.id);
// Create users with different roles
const users = await createUsersWithRoles(organization.id, customRoles);
// Create a team within the organization
const team = await createTeamWithCustomRoles(organization.id, users, customRoles);
console.log("🎉 PBAC organization setup complete!");
console.log(`Organization URL: ${process.env.NEXT_PUBLIC_WEBAPP_URL}/org/${organization.slug}`);
// Display created users with their credentials and permissions
console.log("\n📋 Created Users - Login Credentials & Permissions:");
console.log("=".repeat(80));
users.forEach(({ user, role, customRole }) => {
const password = getPasswordForUser(user.email);
const permissions = getPermissionsForRole(user.email, customRoles);
console.log(`👤 ${user.name}`);
console.log(` 📧 Email: ${user.email}`);
console.log(` 🔑 Password: ${password}`);
console.log(` 👔 Role: ${role}${customRole ? ` (${customRole})` : ""}`);
if (permissions.length > 0) {
console.log(` 🔐 Permissions:`);
permissions.forEach((permission) => {
console.log(`${permission.resource}:${permission.action}`);
});
} else {
console.log(` 🔐 Permissions: Full access (Owner role)`);
}
console.log("");
});
console.log("=".repeat(80));
return {
organization,
customRoles,
users,
team,
};
}
/**
* Creates custom roles with specific permissions for the organization
*/
async function createCustomRoles(organizationId: number) {
console.log("🔐 Creating custom roles with PBAC permissions...");
// Check if roles already exist
const existingRoles = await prisma.role.findMany({
where: { teamId: organizationId },
});
if (existingRoles.length > 0) {
console.log("⚠️ Custom roles already exist for this organization, skipping creation");
return {
eventManager: existingRoles.find((r) => r.name === "Event Manager"),
analytics: existingRoles.find((r) => r.name === "Analytics Specialist"),
teamCoordinator: existingRoles.find((r) => r.name === "Team Coordinator"),
supportAgent: existingRoles.find((r) => r.name === "Support Agent"),
};
}
// Event Manager Role - Can manage event types and bookings
const eventManagerRole = await prisma.role.create({
data: {
id: `event_manager_${organizationId}`,
name: "Event Manager",
description: "Can create and manage event types, view bookings and recordings",
color: "#3B82F6", // Blue
teamId: organizationId,
type: RoleType.CUSTOM,
permissions: {
create: [
// Event Type permissions
{ resource: "eventType", action: "create" },
{ resource: "eventType", action: "read" },
{ resource: "eventType", action: "update" },
{ resource: "eventType", action: "delete" },
// Booking permissions
{ resource: "booking", action: "read" },
{ resource: "booking", action: "update" },
{ resource: "booking", action: "readRecordings" },
// Team read access
{ resource: "team", action: "read" },
// Availability override
{ resource: "availability", action: "override" },
],
},
},
include: {
permissions: true,
},
});
// Analytics Specialist Role - Can view insights and reports
const analyticsRole = await prisma.role.create({
data: {
id: `analytics_specialist_${organizationId}`,
name: "Analytics Specialist",
description: "Can view insights, reports, and booking analytics",
color: "#10B981", // Green
teamId: organizationId,
type: RoleType.CUSTOM,
permissions: {
create: [
// Insights permissions
{ resource: "insights", action: "read" },
// Booking read access
{ resource: "booking", action: "read" },
{ resource: "booking", action: "readTeamBookings" },
{ resource: "booking", action: "readOrgBookings" },
// Event Type read access
{ resource: "eventType", action: "read" },
// Team and organization read access
{ resource: "team", action: "read" },
{ resource: "organization", action: "read" },
{ resource: "organization", action: "listMembers" },
// Routing forms for conversion tracking
{ resource: "routingForm", action: "read" },
],
},
},
include: {
permissions: true,
},
});
// Team Coordinator Role - Can manage team members and workflows
const teamCoordinatorRole = await prisma.role.create({
data: {
id: `team_coordinator_${organizationId}`,
name: "Team Coordinator",
description: "Can invite/remove team members, manage workflows and routing forms",
color: "#8B5CF6", // Purple
teamId: organizationId,
type: RoleType.CUSTOM,
permissions: {
create: [
// Team management
{ resource: "team", action: "read" },
{ resource: "team", action: "update" },
{ resource: "team", action: "invite" },
{ resource: "team", action: "remove" },
{ resource: "team", action: "changeMemberRole" },
// Organization member management
{ resource: "organization", action: "read" },
{ resource: "organization", action: "listMembers" },
// Workflow management
{ resource: "workflow", action: "create" },
{ resource: "workflow", action: "read" },
{ resource: "workflow", action: "update" },
{ resource: "workflow", action: "delete" },
// Routing form management
{ resource: "routingForm", action: "create" },
{ resource: "routingForm", action: "read" },
{ resource: "routingForm", action: "update" },
{ resource: "routingForm", action: "delete" },
// Basic booking access
{ resource: "booking", action: "read" },
{ resource: "eventType", action: "read" },
],
},
},
include: {
permissions: true,
},
});
// Support Agent Role - Limited access for customer support
const supportAgentRole = await prisma.role.create({
data: {
id: `support_agent_${organizationId}`,
name: "Support Agent",
description: "Can view bookings and basic event information for customer support",
color: "#F59E0B", // Amber
teamId: organizationId,
type: RoleType.CUSTOM,
permissions: {
create: [
// Limited booking access
{ resource: "booking", action: "read" },
{ resource: "booking", action: "update" }, // For rescheduling/canceling
// Event Type read access
{ resource: "eventType", action: "read" },
// Team read access
{ resource: "team", action: "read" },
// Organization read access
{ resource: "organization", action: "read" },
],
},
},
include: {
permissions: true,
},
});
console.log("✅ Created custom roles:");
console.log(` - Event Manager (${eventManagerRole.permissions.length} permissions)`);
console.log(` - Analytics Specialist (${analyticsRole.permissions.length} permissions)`);
console.log(` - Team Coordinator (${teamCoordinatorRole.permissions.length} permissions)`);
console.log(` - Support Agent (${supportAgentRole.permissions.length} permissions)`);
return {
eventManager: eventManagerRole,
analytics: analyticsRole,
teamCoordinator: teamCoordinatorRole,
supportAgent: supportAgentRole,
};
}
/**
* Creates users with different roles in the organization
*/
async function createUsersWithRoles(organizationId: number, customRoles: any) {
console.log("👥 Creating users with custom roles...");
const users: Array<{
user: any;
role: MembershipRole;
customRole: string | null;
}> = [];
// Check if users already exist
const existingUsers = await prisma.user.findMany({
where: {
email: {
in: [
"owner@pbac-demo.com",
"events@pbac-demo.com",
"analytics@pbac-demo.com",
"coordinator@pbac-demo.com",
"support@pbac-demo.com",
],
},
},
});
if (existingUsers.length > 0) {
console.log("⚠️ Some PBAC users already exist, skipping user creation");
return existingUsers.map((user) => ({
user,
role: MembershipRole.MEMBER,
customRole: null,
}));
}
// Organization Owner
const owner = await createUser({
email: "owner@pbac-demo.com",
username: "pbac-owner",
name: "Organization Owner",
password: "pbac-owner-2024!",
});
await createMembership({
userId: owner.id,
teamId: organizationId,
role: MembershipRole.OWNER,
customRoleId: null, // Uses default owner role
});
await createProfile({
userId: owner.id,
organizationId,
username: "owner",
});
users.push({ user: owner, role: MembershipRole.OWNER, customRole: null });
// Event Manager
const eventManager = await createUser({
email: "events@pbac-demo.com",
username: "pbac-events",
name: "Event Manager",
password: "events-2024!",
});
await createMembership({
userId: eventManager.id,
teamId: organizationId,
role: MembershipRole.MEMBER,
customRoleId: customRoles.eventManager.id,
});
await createProfile({
userId: eventManager.id,
organizationId,
username: "events",
});
users.push({ user: eventManager, role: MembershipRole.MEMBER, customRole: "Event Manager" });
// Analytics Specialist
const analytics = await createUser({
email: "analytics@pbac-demo.com",
username: "pbac-analytics",
name: "Analytics Specialist",
password: "analytics-2024!",
});
await createMembership({
userId: analytics.id,
teamId: organizationId,
role: MembershipRole.MEMBER,
customRoleId: customRoles.analytics.id,
});
await createProfile({
userId: analytics.id,
organizationId,
username: "analytics",
});
users.push({ user: analytics, role: MembershipRole.MEMBER, customRole: "Analytics Specialist" });
// Team Coordinator
const coordinator = await createUser({
email: "coordinator@pbac-demo.com",
username: "pbac-coordinator",
name: "Team Coordinator",
password: "coordinator-2024!",
});
await createMembership({
userId: coordinator.id,
teamId: organizationId,
role: MembershipRole.ADMIN,
customRoleId: customRoles.teamCoordinator.id,
});
await createProfile({
userId: coordinator.id,
organizationId,
username: "coordinator",
});
users.push({ user: coordinator, role: MembershipRole.ADMIN, customRole: "Team Coordinator" });
// Support Agent
const support = await createUser({
email: "support@pbac-demo.com",
username: "pbac-support",
name: "Support Agent",
password: "support-2024!",
});
await createMembership({
userId: support.id,
teamId: organizationId,
role: MembershipRole.MEMBER,
customRoleId: customRoles.supportAgent.id,
});
await createProfile({
userId: support.id,
organizationId,
username: "support",
});
users.push({ user: support, role: MembershipRole.MEMBER, customRole: "Support Agent" });
console.log("✅ Created users with roles:");
users.forEach(({ user, role, customRole }) => {
console.log(` - ${user.name} (${user.email}) - ${role}${customRole ? ` + ${customRole}` : ""}`);
});
return users;
}
/**
* Creates a team within the organization with custom role assignments
*/
async function createTeamWithCustomRoles(organizationId: number, users: any[], customRoles: any) {
console.log("🏢 Creating team with custom role assignments...");
const team = await prisma.team.create({
data: {
name: "Sales Team",
slug: "sales-team",
parentId: organizationId,
metadata: {},
},
});
// Add team members with their custom roles
for (const { user } of users) {
let customRoleId: string | null = null;
// Assign appropriate roles for the team
if (user.email === "owner@pbac-demo.com") {
customRoleId = "owner_role";
} else if (user.email === "coordinator@pbac-demo.com") {
customRoleId = customRoles.teamCoordinator.id;
} else if (user.email === "events@pbac-demo.com") {
customRoleId = customRoles.eventManager.id;
} else if (user.email === "analytics@pbac-demo.com") {
customRoleId = customRoles.analytics.id;
} else if (user.email === "support@pbac-demo.com") {
customRoleId = customRoles.supportAgent.id;
}
await prisma.membership.create({
data: {
userId: user.id,
teamId: team.id,
role: MembershipRole.MEMBER,
customRoleId,
accepted: true,
},
});
}
// Create some event types for the team
await prisma.eventType.create({
data: {
title: "Sales Consultation",
slug: "sales-consultation",
length: 30,
teamId: team.id,
userId: users[0].user.id, // Owner as the creator
users: {
connect: users.map(({ user }) => ({ id: user.id })),
},
},
});
await prisma.eventType.create({
data: {
title: "Product Demo",
slug: "product-demo",
length: 45,
teamId: team.id,
userId: users[0].user.id, // Owner as the creator
users: {
connect: users.slice(0, 3).map(({ user }) => ({ id: user.id })), // First 3 users
},
},
});
console.log(`✅ Created team: ${team.name} with ${users.length} members`);
console.log(`Team URL: ${process.env.NEXT_PUBLIC_WEBAPP_URL}/team/${team.slug}`);
return team;
}
/**
* Helper function to create a user
*/
async function createUser(userData: { email: string; username: string; name: string; password: string }) {
const user = await prisma.user.create({
data: {
email: userData.email,
username: userData.username,
name: userData.name,
emailVerified: new Date(),
completedOnboarding: true,
locale: "en",
schedules: {
create: {
name: "Working Hours",
availability: {
createMany: {
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
},
},
},
},
},
});
await prisma.userPassword.create({
data: {
userId: user.id,
hash: await hashPassword(userData.password),
},
});
return user;
}
/**
* Helper function to create a membership
*/
async function createMembership({
userId,
teamId,
role,
customRoleId,
}: {
userId: number;
teamId: number;
role: MembershipRole;
customRoleId: string | null;
}) {
return prisma.membership.create({
data: {
userId,
teamId,
role,
customRoleId,
accepted: true,
},
});
}
/**
* Helper function to create a profile
*/
async function createProfile({
userId,
organizationId,
username,
}: {
userId: number;
organizationId: number;
username: string;
}) {
const profile = await prisma.profile.create({
data: {
uid: uuid(),
userId,
organizationId,
username,
},
});
// Update user to point to this profile
await prisma.user.update({
where: { id: userId },
data: { movedToProfileId: profile.id },
});
return profile;
}
/**
* Helper function to get password for a user based on their email
*/
function getPasswordForUser(email: string): string {
const passwordMap: Record<string, string> = {
"owner@pbac-demo.com": "pbac-owner-2024!",
"events@pbac-demo.com": "events-2024!",
"analytics@pbac-demo.com": "analytics-2024!",
"coordinator@pbac-demo.com": "coordinator-2024!",
"support@pbac-demo.com": "support-2024!",
};
return passwordMap[email] || "unknown";
}
/**
* Helper function to get permissions for a role based on user email
*/
function getPermissionsForRole(email: string, customRoles: any): Array<{ resource: string; action: string }> {
// Owner has full access, no specific permissions to list
if (email === "owner@pbac-demo.com") {
return [];
}
// Map email to role and return permissions
const rolePermissionMap: Record<string, Array<{ resource: string; action: string }>> = {
"events@pbac-demo.com": [
{ resource: "eventType", action: "create" },
{ resource: "eventType", action: "read" },
{ resource: "eventType", action: "update" },
{ resource: "eventType", action: "delete" },
{ resource: "booking", action: "read" },
{ resource: "booking", action: "update" },
{ resource: "booking", action: "readRecordings" },
{ resource: "team", action: "read" },
{ resource: "availability", action: "override" },
],
"analytics@pbac-demo.com": [
{ resource: "insights", action: "read" },
{ resource: "booking", action: "read" },
{ resource: "booking", action: "readTeamBookings" },
{ resource: "booking", action: "readOrgBookings" },
{ resource: "eventType", action: "read" },
{ resource: "team", action: "read" },
{ resource: "organization", action: "read" },
{ resource: "organization", action: "listMembers" },
{ resource: "routingForm", action: "read" },
],
"coordinator@pbac-demo.com": [
{ resource: "team", action: "read" },
{ resource: "team", action: "update" },
{ resource: "team", action: "invite" },
{ resource: "team", action: "remove" },
{ resource: "team", action: "changeMemberRole" },
{ resource: "organization", action: "read" },
{ resource: "organization", action: "listMembers" },
{ resource: "workflow", action: "create" },
{ resource: "workflow", action: "read" },
{ resource: "workflow", action: "update" },
{ resource: "workflow", action: "delete" },
{ resource: "routingForm", action: "create" },
{ resource: "routingForm", action: "read" },
{ resource: "routingForm", action: "update" },
{ resource: "routingForm", action: "delete" },
{ resource: "booking", action: "read" },
{ resource: "eventType", action: "read" },
],
"support@pbac-demo.com": [
{ resource: "booking", action: "read" },
{ resource: "booking", action: "update" },
{ resource: "eventType", action: "read" },
{ resource: "team", action: "read" },
{ resource: "organization", action: "read" },
],
};
return rolePermissionMap[email] || [];
}
// Run the function if this file is executed directly
if (require.main === module) {
createPBACOrganization()
.then(() => {
console.log("✅ PBAC organization created successfully!");
process.exit(0);
})
.catch((error) => {
console.error("❌ Error creating PBAC organization:", error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
}
+13 -2
View File
@@ -342,8 +342,19 @@ async function createOrganizationAndAddMembersAndTeams({
orgProfile: member.orgProfile,
};
await prisma.tempOrgRedirect.create({
data: {
// Create temp org redirect with upsert to handle duplicates
await prisma.tempOrgRedirect.upsert({
where: {
from_type_fromOrgId: {
from: member.memberData.username,
type: RedirectType.User,
fromOrgId: 0,
},
},
update: {
toUrl: `${getOrgFullOrigin(orgData.slug)}/${member.orgProfile.username}`,
},
create: {
fromOrgId: 0,
type: RedirectType.User,
from: member.memberData.username,