forked from EduCraft/curriculum-project-hub
feat: add deployable alpha silo
This commit is contained in:
@@ -5,6 +5,7 @@ import type { Folder, OrganizationMemberRole, PermissionRole, Prisma, PrismaClie
|
||||
import { createPermissionAuthorizer } from "./permission.js";
|
||||
import { writeAudit } from "./audit.js";
|
||||
import { lockActiveOrganization, requireActiveOrganizationStatus } from "./org/status.js";
|
||||
import { scopedFeishuPrincipalId } from "./feishu/identityNamespace.js";
|
||||
|
||||
export interface OrganizationProjectPolicy {
|
||||
readonly organizationId: string;
|
||||
@@ -140,7 +141,8 @@ export async function createProjectFromOrgAdmin(
|
||||
return createManagedProject(prisma, {
|
||||
organizationId: input.organizationId,
|
||||
actorUserId: actor.userId,
|
||||
actorFeishuOpenId: input.actorFeishuOpenId,
|
||||
actorPrincipalId: actor.principalId,
|
||||
feishuConnectionId: actor.connectionId,
|
||||
name,
|
||||
workspaceRoot: input.workspaceRoot,
|
||||
folderId: input.folderId,
|
||||
@@ -170,7 +172,8 @@ export async function createProjectFromFeishuChat(
|
||||
return createManagedProject(prisma, {
|
||||
organizationId: input.organizationId,
|
||||
actorUserId: actor.userId,
|
||||
actorFeishuOpenId: input.actorFeishuOpenId,
|
||||
actorPrincipalId: actor.principalId,
|
||||
feishuConnectionId: actor.connectionId,
|
||||
name,
|
||||
workspaceRoot: input.workspaceRoot,
|
||||
folderId: input.folderId,
|
||||
@@ -212,7 +215,7 @@ export async function bindFeishuChatToProject(
|
||||
await replaceProjectGrant(tx, {
|
||||
projectId: project.id,
|
||||
principalType: "FEISHU_CHAT",
|
||||
principalId: chatId,
|
||||
principalId: scopedChatPrincipalId(actor.connectionId, chatId),
|
||||
role: "EDIT",
|
||||
createdByUserId: actor.userId,
|
||||
});
|
||||
@@ -262,7 +265,7 @@ export async function archiveFeishuChatBinding(
|
||||
resourceType: "PROJECT",
|
||||
resourceId: binding.projectId,
|
||||
principalType: "FEISHU_CHAT",
|
||||
principalId: binding.chatId,
|
||||
principalId: scopedChatPrincipalId(actor.connectionId, binding.chatId),
|
||||
revokedAt: null,
|
||||
},
|
||||
data: { revokedAt: new Date() },
|
||||
@@ -282,7 +285,8 @@ async function createManagedProject(
|
||||
input: {
|
||||
readonly organizationId: string;
|
||||
readonly actorUserId: string;
|
||||
readonly actorFeishuOpenId: string;
|
||||
readonly actorPrincipalId: string;
|
||||
readonly feishuConnectionId: string | undefined;
|
||||
readonly name: string;
|
||||
readonly workspaceRoot: string;
|
||||
readonly folderId: string | undefined;
|
||||
@@ -332,7 +336,7 @@ async function createManagedProject(
|
||||
await replaceProjectGrant(tx, {
|
||||
projectId: created.id,
|
||||
principalType: "USER",
|
||||
principalId: input.actorFeishuOpenId,
|
||||
principalId: input.actorPrincipalId,
|
||||
role: "MANAGE",
|
||||
createdByUserId: input.actorUserId,
|
||||
});
|
||||
@@ -343,7 +347,7 @@ async function createManagedProject(
|
||||
await replaceProjectGrant(tx, {
|
||||
projectId: created.id,
|
||||
principalType: "FEISHU_CHAT",
|
||||
principalId: input.chatId,
|
||||
principalId: scopedChatPrincipalId(input.feishuConnectionId, input.chatId),
|
||||
role: "EDIT",
|
||||
createdByUserId: input.actorUserId,
|
||||
});
|
||||
@@ -385,7 +389,13 @@ async function requireProjectManager(
|
||||
projectId: string,
|
||||
organizationId: string,
|
||||
actorFeishuOpenId: string,
|
||||
): Promise<{ readonly userId: string; readonly role: OrganizationMemberRole; readonly via: "org-admin" | "project-manage" }> {
|
||||
): Promise<{
|
||||
readonly userId: string;
|
||||
readonly role: OrganizationMemberRole;
|
||||
readonly principalId: string;
|
||||
readonly connectionId: string | undefined;
|
||||
readonly via: "org-admin" | "project-manage";
|
||||
}> {
|
||||
const actor = await requireActiveOrgMember(prisma, organizationId, actorFeishuOpenId);
|
||||
if (isOrgAdminRole(actor.role)) {
|
||||
return { ...actor, via: "org-admin" };
|
||||
@@ -405,9 +415,42 @@ async function requireActiveOrgMember(
|
||||
prisma: PrismaClient,
|
||||
organizationId: string,
|
||||
feishuOpenId: string,
|
||||
): Promise<{ readonly userId: string; readonly role: OrganizationMemberRole }> {
|
||||
): Promise<{
|
||||
readonly userId: string;
|
||||
readonly role: OrganizationMemberRole;
|
||||
readonly principalId: string;
|
||||
readonly connectionId: string | undefined;
|
||||
}> {
|
||||
await requireActiveOrganization(prisma, organizationId);
|
||||
const user = await prisma.user.findUnique({
|
||||
const identity = await prisma.feishuUserIdentity.findFirst({
|
||||
where: { openId: feishuOpenId, connection: { organizationId, status: "ACTIVE" } },
|
||||
select: {
|
||||
connectionId: true,
|
||||
user: { select: {
|
||||
id: true,
|
||||
organizationMemberships: {
|
||||
where: { organizationId, revokedAt: null },
|
||||
select: { role: true },
|
||||
take: 1,
|
||||
},
|
||||
} },
|
||||
},
|
||||
});
|
||||
if (identity !== null) {
|
||||
const membership = identity.user.organizationMemberships[0];
|
||||
if (membership === undefined) {
|
||||
throw new Error(`Feishu user ${feishuOpenId} is not an active member of organization ${organizationId}`);
|
||||
}
|
||||
return {
|
||||
userId: identity.user.id,
|
||||
role: membership.role,
|
||||
principalId: scopedFeishuPrincipalId("USER", identity.connectionId, feishuOpenId),
|
||||
connectionId: identity.connectionId,
|
||||
};
|
||||
}
|
||||
// Explicit compatibility path for pre-ADR-0024 rows while the Silo branch
|
||||
// completes migration. New bootstrap/member writes always create identities.
|
||||
const user = process.env["NODE_ENV"] === "production" ? null : await prisma.user.findUnique({
|
||||
where: { feishuOpenId },
|
||||
select: {
|
||||
id: true,
|
||||
@@ -425,7 +468,11 @@ async function requireActiveOrgMember(
|
||||
if (membership === undefined) {
|
||||
throw new Error(`Feishu user ${feishuOpenId} is not an active member of organization ${organizationId}`);
|
||||
}
|
||||
return { userId: user.id, role: membership.role };
|
||||
return { userId: user.id, role: membership.role, principalId: feishuOpenId, connectionId: undefined };
|
||||
}
|
||||
|
||||
function scopedChatPrincipalId(connectionId: string | undefined, chatId: string): string {
|
||||
return connectionId === undefined ? chatId : scopedFeishuPrincipalId("CHAT", connectionId, chatId);
|
||||
}
|
||||
|
||||
async function ensureOrganizationProjectSettingsTx(
|
||||
|
||||
Reference in New Issue
Block a user