forked from bai/curriculum-project-hub
136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
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;
|
|
}
|
|
}
|