forked from bai/curriculum-project-hub
feat(hub): 支持教师团队权限
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import type { PrismaClient, PrincipalType } from "@prisma/client";
|
||||
|
||||
export type ExternalPrincipalType = Extract<
|
||||
PrincipalType,
|
||||
"FEISHU_CHAT" | "FEISHU_DEPARTMENT" | "FEISHU_USER_GROUP"
|
||||
>;
|
||||
|
||||
export interface ExternalPrincipalMembershipInput {
|
||||
readonly feishuOpenId: string;
|
||||
readonly displayName: string;
|
||||
readonly principalType: ExternalPrincipalType;
|
||||
readonly principalId: string;
|
||||
}
|
||||
|
||||
export interface SyncExternalPrincipalMembershipsInput {
|
||||
readonly source: string;
|
||||
readonly replaceSource?: boolean | undefined;
|
||||
readonly memberships: readonly ExternalPrincipalMembershipInput[];
|
||||
}
|
||||
|
||||
export interface SyncExternalPrincipalMembershipsResult {
|
||||
readonly synced: number;
|
||||
readonly revoked: number;
|
||||
}
|
||||
|
||||
export async function syncExternalPrincipalMemberships(
|
||||
prisma: PrismaClient,
|
||||
input: SyncExternalPrincipalMembershipsInput,
|
||||
): Promise<SyncExternalPrincipalMembershipsResult> {
|
||||
const syncedAt = new Date();
|
||||
const incomingKeys = new Set<string>();
|
||||
let synced = 0;
|
||||
|
||||
for (const item of input.memberships) {
|
||||
assertExternalPrincipalType(item.principalType);
|
||||
const user = await prisma.user.upsert({
|
||||
where: { feishuOpenId: item.feishuOpenId },
|
||||
update: { displayName: item.displayName },
|
||||
create: {
|
||||
feishuOpenId: item.feishuOpenId,
|
||||
displayName: item.displayName,
|
||||
platformRoles: { create: { role: "TEACHER" } },
|
||||
},
|
||||
});
|
||||
incomingKeys.add(externalMembershipKey(user.id, item.principalType, item.principalId));
|
||||
const existing = await prisma.externalPrincipalMembership.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
principalType: item.principalType,
|
||||
principalId: item.principalId,
|
||||
source: input.source,
|
||||
revokedAt: null,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing === null) {
|
||||
await prisma.externalPrincipalMembership.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
principalType: item.principalType,
|
||||
principalId: item.principalId,
|
||||
source: input.source,
|
||||
syncedAt,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.externalPrincipalMembership.update({
|
||||
where: { id: existing.id },
|
||||
data: { syncedAt },
|
||||
});
|
||||
}
|
||||
synced++;
|
||||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
const revokeIds = active
|
||||
.filter((membership) => !incomingKeys.has(externalMembershipKey(
|
||||
membership.userId,
|
||||
membership.principalType,
|
||||
membership.principalId,
|
||||
)))
|
||||
.map((membership) => membership.id);
|
||||
if (revokeIds.length > 0) {
|
||||
const result = await prisma.externalPrincipalMembership.updateMany({
|
||||
where: { id: { in: revokeIds } },
|
||||
data: { revokedAt: syncedAt },
|
||||
});
|
||||
revoked = result.count;
|
||||
}
|
||||
}
|
||||
|
||||
return { synced, revoked };
|
||||
}
|
||||
|
||||
function externalMembershipKey(userId: string, type: PrincipalType, id: string): string {
|
||||
return `${userId}:${type}:${id}`;
|
||||
}
|
||||
|
||||
function assertExternalPrincipalType(type: PrincipalType): asserts type is ExternalPrincipalType {
|
||||
if (type !== "FEISHU_CHAT" && type !== "FEISHU_DEPARTMENT" && type !== "FEISHU_USER_GROUP") {
|
||||
throw new Error(`unsupported external principal type: ${type}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user