Merge branch 'maoyuanyang-main'

# Conflicts:
#	hub/filelib-web/src/lib/GrantsPanel.svelte
#	hub/filelib-web/src/lib/OverviewPanel.svelte
#	hub/filelib-web/src/lib/types.ts
#	hub/src/database/filelib/grantService.ts
This commit is contained in:
2026-08-03 15:35:44 +08:00
37 changed files with 1926 additions and 273 deletions
+37 -50
View File
@@ -25,13 +25,10 @@ export interface GrantDto {
readonly id: string;
readonly principalType: "USER" | "GROUP";
readonly principalId: string;
/**
* 展示名(ADR-0029:后端负责把 id 解析成人看的名字,前端不二次查询)。
* USER → `User.displayName`;GROUP → `MemberGroup.name`;
* 取不到行(用户/组已删)时回落为 principalId,与 `/database/api/me` 同一回落语义。
* 纯展示字段:写路径仍只认 principalId,不得用它做任何授权判断。
*/
readonly principalName: string;
/** 主体显示名(用户 displayName / 组 name);主体已删时为 null,前端回落 principalId。 */
readonly principalName: string | null;
/** USER 主体的飞书 openId;GROUP 或主体已删时为 null。 */
readonly principalOpenId: string | null;
readonly role: FileLibRole;
readonly isCreatorGrant: boolean;
readonly createdAt: Date;
@@ -42,13 +39,42 @@ function toDto(grant: FileLibGrant, principalName?: string): GrantDto {
id: grant.id,
principalType: grant.principalType,
principalId: grant.principalId,
principalName: principalName ?? grant.principalId,
principalName: null,
principalOpenId: null,
role: grant.role,
isCreatorGrant: grant.isCreatorGrant,
createdAt: grant.createdAt,
};
}
/** 批量回填主体显示名与飞书 openId(两次查询,不做 per-row 往返)。可在事务内调用。 */
async function withPrincipalNames(
prisma: Pick<PrismaClient, "user" | "memberGroup">,
grants: readonly GrantDto[],
): Promise<readonly GrantDto[]> {
const userIds = [...new Set(grants.filter((g) => g.principalType === "USER").map((g) => g.principalId))];
const groupIds = [...new Set(grants.filter((g) => g.principalType === "GROUP").map((g) => g.principalId))];
const users = userIds.length === 0
? []
: await prisma.user.findMany({
where: { id: { in: userIds } },
select: { id: true, displayName: true, feishuOpenId: true },
});
const groups = groupIds.length === 0
? []
: await prisma.memberGroup.findMany({ where: { id: { in: groupIds } }, select: { id: true, name: true } });
const nameById = new Map<string, string>([
...users.map((u) => [u.id, u.displayName] as const),
...groups.map((g) => [g.id, g.name] as const),
]);
const openIdById = new Map<string, string>(users.map((u) => [u.id, u.feishuOpenId] as const));
return grants.map((g) => ({
...g,
principalName: nameById.get(g.principalId) ?? null,
principalOpenId: g.principalType === "USER" ? openIdById.get(g.principalId) ?? null : null,
}));
}
type Tx = Prisma.TransactionClient;
type Deps = AccessDeps & { readonly prisma: PrismaClient };
@@ -103,7 +129,7 @@ export async function listGrants(
where: { organizationId: deps.organizationId, nodeId, revokedAt: null },
orderBy: [{ isCreatorGrant: "desc" }, { createdAt: "asc" }],
});
return toDtosWithNames(deps.prisma, grants);
return withPrincipalNames(deps.prisma, grants.map(toDto));
}
export interface PutGrantsResult {
@@ -174,7 +200,7 @@ export async function putGrants(
where: { organizationId: deps.organizationId, nodeId: node.id, revokedAt: null },
orderBy: [{ isCreatorGrant: "desc" }, { createdAt: "asc" }],
});
return { granted, updated, grants: await toDtosWithNames(tx, grants) };
return { granted, updated, grants: await withPrincipalNames(tx, grants.map(toDto)) };
});
}
@@ -273,46 +299,7 @@ export async function forceAdjustGrants(
where: { organizationId: deps.organizationId, nodeId: node.id, revokedAt: null },
orderBy: [{ isCreatorGrant: "desc" }, { createdAt: "asc" }],
});
return { granted, updated, grants: await toDtosWithNames(tx, grants) };
});
}
/** 项目独立权限开关(P5/D11):需 MANAGE;状态不变则空操作。 */
export async function setIndependentPermission(
deps: Deps,
actor: FileLibActor,
nodeId: string,
enabled: boolean,
): Promise<{ readonly enabled: boolean }> {
return deps.prisma.$transaction(async (tx) => {
const { node } = await requireManage(deps, actor, nodeId, tx);
if (node.kind !== "PROJECT") {
throw new FileLibError(400, "invalid_node_kind", "independent permission applies to projects only");
}
const current = await tx.fileLibProjectSettings.findUnique({
where: { nodeId: node.id },
select: { independentPermissionsEnabled: true },
});
if ((current?.independentPermissionsEnabled ?? false) === enabled) {
return { enabled }; // 状态未变:空操作,不产生审计
}
await tx.fileLibProjectSettings.upsert({
where: { nodeId: node.id },
update: { independentPermissionsEnabled: enabled },
create: { nodeId: node.id, independentPermissionsEnabled: enabled },
});
await writeFileLibAudit(tx, {
action: enabled
? FILE_LIB_AUDIT_ACTIONS.independentEnable
: FILE_LIB_AUDIT_ACTIONS.independentDisable,
actorUserId: actor.userId,
organizationId: deps.organizationId,
objectType: "project",
objectId: node.id,
objectPath: node.pathIds,
detail: { enabled },
});
return { enabled };
return { granted, updated, grants: await withPrincipalNames(tx, grants.map(toDto)) };
});
}