forked from bai/curriculum-project-hub
feat(hub): 支持教师团队权限
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import type { PrismaClient, PrincipalType } from "@prisma/client";
|
||||
|
||||
export interface PrincipalRef {
|
||||
readonly type: PrincipalType;
|
||||
readonly id: string;
|
||||
}
|
||||
|
||||
export interface ActorInput {
|
||||
readonly feishuOpenId: string;
|
||||
readonly chatId?: string | undefined;
|
||||
}
|
||||
|
||||
export type PrincipalSource =
|
||||
| "actor-user"
|
||||
| "context-chat"
|
||||
| "team-membership"
|
||||
| "external-membership"
|
||||
| "team-external-binding";
|
||||
|
||||
export interface ResolvedPrincipal {
|
||||
readonly principal: PrincipalRef;
|
||||
readonly source: PrincipalSource;
|
||||
readonly via?: PrincipalRef | undefined;
|
||||
}
|
||||
|
||||
export interface PrincipalResolution {
|
||||
readonly actor: ActorInput;
|
||||
readonly userId?: string | undefined;
|
||||
readonly principals: readonly PrincipalRef[];
|
||||
readonly resolved: readonly ResolvedPrincipal[];
|
||||
}
|
||||
|
||||
export interface PrincipalResolver {
|
||||
resolveActor(actor: ActorInput): Promise<PrincipalResolution>;
|
||||
}
|
||||
|
||||
export class PrismaPrincipalResolver implements PrincipalResolver {
|
||||
constructor(private readonly prisma: PrismaClient) {}
|
||||
|
||||
async resolveActor(actor: ActorInput): Promise<PrincipalResolution> {
|
||||
const resolved = new PrincipalCollector();
|
||||
resolved.add({ type: "USER", id: actor.feishuOpenId }, "actor-user");
|
||||
if (actor.chatId !== undefined && actor.chatId !== "") {
|
||||
resolved.add({ type: "FEISHU_CHAT", id: actor.chatId }, "context-chat");
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { feishuOpenId: actor.feishuOpenId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (user !== null) {
|
||||
const memberships = await this.prisma.teamMembership.findMany({
|
||||
where: { userId: user.id, revokedAt: null, team: { archivedAt: null } },
|
||||
select: { teamId: true },
|
||||
});
|
||||
for (const membership of memberships) {
|
||||
resolved.add({ type: "TEAM", id: membership.teamId }, "team-membership");
|
||||
}
|
||||
|
||||
const externalMemberships = await this.prisma.externalPrincipalMembership.findMany({
|
||||
where: { userId: user.id, revokedAt: null },
|
||||
select: { principalType: true, principalId: true },
|
||||
});
|
||||
for (const membership of externalMemberships) {
|
||||
resolved.add(
|
||||
{ type: membership.principalType, id: membership.principalId },
|
||||
"external-membership",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const externalPrincipals = resolved.principals().filter((principal) => isExternalPrincipal(principal.type));
|
||||
if (externalPrincipals.length > 0) {
|
||||
const bindings = await this.prisma.teamExternalBinding.findMany({
|
||||
where: {
|
||||
revokedAt: null,
|
||||
team: { archivedAt: null },
|
||||
OR: externalPrincipals.map((principal) => ({
|
||||
principalType: principal.type,
|
||||
principalId: principal.id,
|
||||
})),
|
||||
},
|
||||
select: { teamId: true, principalType: true, principalId: true },
|
||||
});
|
||||
for (const binding of bindings) {
|
||||
resolved.add(
|
||||
{ type: "TEAM", id: binding.teamId },
|
||||
"team-external-binding",
|
||||
{ type: binding.principalType, id: binding.principalId },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
actor,
|
||||
...(user !== null ? { userId: user.id } : {}),
|
||||
principals: resolved.principals(),
|
||||
resolved: resolved.entries(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function principalKey(principal: PrincipalRef): string {
|
||||
return `${principal.type}:${principal.id}`;
|
||||
}
|
||||
|
||||
function isExternalPrincipal(type: PrincipalType): boolean {
|
||||
return type === "FEISHU_CHAT" || type === "FEISHU_DEPARTMENT" || type === "FEISHU_USER_GROUP";
|
||||
}
|
||||
|
||||
class PrincipalCollector {
|
||||
private readonly refs = new Map<string, PrincipalRef>();
|
||||
private readonly provenance: ResolvedPrincipal[] = [];
|
||||
|
||||
add(principal: PrincipalRef, source: PrincipalSource, via?: PrincipalRef): void {
|
||||
const key = principalKey(principal);
|
||||
if (!this.refs.has(key)) {
|
||||
this.refs.set(key, principal);
|
||||
}
|
||||
this.provenance.push({
|
||||
principal,
|
||||
source,
|
||||
...(via !== undefined ? { via } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
principals(): readonly PrincipalRef[] {
|
||||
return [...this.refs.values()];
|
||||
}
|
||||
|
||||
entries(): readonly ResolvedPrincipal[] {
|
||||
return this.provenance;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user