feat: add organization tenant model

This commit is contained in:
2026-07-09 23:37:20 +08:00
parent 5d315fff21
commit 2b7aa3294f
20 changed files with 900 additions and 116 deletions
+48 -7
View File
@@ -13,6 +13,9 @@ export interface ExternalPrincipalMembershipInput {
}
export interface SyncExternalPrincipalMembershipsInput {
readonly organizationId: string;
readonly provider?: string | undefined;
readonly providerTenantId?: string | undefined;
readonly source: string;
readonly replaceSource?: boolean | undefined;
readonly memberships: readonly ExternalPrincipalMembershipInput[];
@@ -30,6 +33,26 @@ export async function syncExternalPrincipalMemberships(
const syncedAt = new Date();
const incomingKeys = new Set<string>();
let synced = 0;
const connection = await prisma.externalDirectoryConnection.upsert({
where: {
organizationId_provider_source: {
organizationId: input.organizationId,
provider: input.provider ?? "FEISHU",
source: input.source,
},
},
update: {
revokedAt: null,
...(input.providerTenantId !== undefined ? { providerTenantId: input.providerTenantId } : {}),
},
create: {
organizationId: input.organizationId,
provider: input.provider ?? "FEISHU",
...(input.providerTenantId !== undefined ? { providerTenantId: input.providerTenantId } : {}),
source: input.source,
},
select: { id: true },
});
for (const item of input.memberships) {
assertExternalPrincipalType(item.principalType);
@@ -40,15 +63,17 @@ export async function syncExternalPrincipalMemberships(
feishuOpenId: item.feishuOpenId,
displayName: item.displayName,
platformRoles: { create: { role: "TEACHER" } },
organizationMemberships: { create: { organizationId: input.organizationId, role: "MEMBER" } },
},
});
incomingKeys.add(externalMembershipKey(user.id, item.principalType, item.principalId));
await ensureOrganizationMembership(prisma, input.organizationId, user.id);
incomingKeys.add(externalMembershipKey(user.id, item.principalType, item.principalId, connection.id));
const existing = await prisma.externalPrincipalMembership.findFirst({
where: {
userId: user.id,
principalType: item.principalType,
principalId: item.principalId,
source: input.source,
connectionId: connection.id,
revokedAt: null,
},
select: { id: true },
@@ -57,9 +82,9 @@ export async function syncExternalPrincipalMemberships(
await prisma.externalPrincipalMembership.create({
data: {
userId: user.id,
connectionId: connection.id,
principalType: item.principalType,
principalId: item.principalId,
source: input.source,
syncedAt,
},
});
@@ -75,14 +100,15 @@ export async function syncExternalPrincipalMemberships(
let revoked = 0;
if (input.replaceSource === true) {
const active = await prisma.externalPrincipalMembership.findMany({
where: { source: input.source, revokedAt: null },
select: { id: true, userId: true, principalType: true, principalId: true },
where: { connectionId: connection.id, revokedAt: null },
select: { id: true, userId: true, principalType: true, principalId: true, connectionId: true },
});
const revokeIds = active
.filter((membership) => !incomingKeys.has(externalMembershipKey(
membership.userId,
membership.principalType,
membership.principalId,
membership.connectionId,
)))
.map((membership) => membership.id);
if (revokeIds.length > 0) {
@@ -97,8 +123,23 @@ export async function syncExternalPrincipalMemberships(
return { synced, revoked };
}
function externalMembershipKey(userId: string, type: PrincipalType, id: string): string {
return `${userId}:${type}:${id}`;
async function ensureOrganizationMembership(
prisma: PrismaClient,
organizationId: string,
userId: string,
): Promise<void> {
const active = await prisma.organizationMembership.findFirst({
where: { organizationId, userId, revokedAt: null },
select: { id: true },
});
if (active !== null) return;
await prisma.organizationMembership.create({
data: { organizationId, userId, role: "MEMBER" },
});
}
function externalMembershipKey(userId: string, type: PrincipalType, id: string, connectionId: string): string {
return `${connectionId}:${userId}:${type}:${id}`;
}
function assertExternalPrincipalType(type: PrincipalType): asserts type is ExternalPrincipalType {