feat: harden tenant access and production operations
This commit is contained in:
@@ -92,6 +92,7 @@ ensureColumn("projects", "archived_at", "TEXT");
|
||||
ensureColumn("projects", "archived_by", "TEXT");
|
||||
ensureColumn("projects", "archived_from_status", "TEXT");
|
||||
ensureColumn("projects", "template_id", "TEXT NOT NULL DEFAULT 'ai-manhua-drama'");
|
||||
ensureColumn("workspace_members", "access_mode", "TEXT NOT NULL DEFAULT 'all'");
|
||||
ensureColumn("auth_sessions", "device_id", "TEXT");
|
||||
ensureColumn("auth_sessions", "risk_level", "TEXT NOT NULL DEFAULT 'medium'");
|
||||
ensureColumn("auth_sessions", "risk_score", "INTEGER NOT NULL DEFAULT 50");
|
||||
@@ -200,6 +201,7 @@ function seedRoles() {
|
||||
["art_director", "workspace", "资产美术", "角色、场景、道具、prompt 和连续性"],
|
||||
["voice_editor", "workspace", "配音/字幕", "固定声线、TTS、字幕和 ASR"],
|
||||
["reviewer", "workspace", "审片", "QA、审片意见和通过/驳回"],
|
||||
["project_guest", "workspace", "项目受限成员", "只访问被授权项目,不继承工作区其他项目"],
|
||||
["project_editor", "project", "项目编辑", "指定项目的内容编辑"],
|
||||
["project_viewer", "project", "项目查看者", "只读查看和下载授权交付物"]
|
||||
];
|
||||
@@ -263,6 +265,7 @@ function seedRoles() {
|
||||
art_director: ["asset:edit", "prompt:edit", "task:view", "task:complete", "job:create"],
|
||||
voice_editor: ["voice:edit", "voice:approve", "task:view", "task:complete", "job:create"],
|
||||
reviewer: ["script:read", "task:view", "task:complete", "qa:review", "voice:approve", "delivery:view"],
|
||||
project_guest: ["script:read", "task:view", "delivery:view"],
|
||||
project_editor: ["script:read", "script:edit", "asset:edit", "prompt:edit", "voice:edit", "task:view", "task:manage", "task:complete", "job:create", "delivery:view"],
|
||||
project_viewer: ["script:read", "task:view", "delivery:view"]
|
||||
};
|
||||
|
||||
@@ -491,6 +491,10 @@ function assertSingleFrameResult(job, result) {
|
||||
export async function executeGenerationJob(context, jobId, body = {}) {
|
||||
const job = jobRow(context, jobId);
|
||||
if (!hasPermission(context, "job:create") && !hasPermission(context, "queue:manage")) throw httpError(403, "permission_denied", "没有执行生成任务的权限");
|
||||
// The local Worker and a manual run request can legitimately race. Once a
|
||||
// job is complete, treating a second run request as an idempotent read keeps
|
||||
// the operator action safe without executing the model twice.
|
||||
if (job.status === "completed") return { job: jobPayload(job), jobs: listGenerationJobs(context), idempotent: true };
|
||||
requireProjectWritable(context);
|
||||
if (!["queued", "blocked", "failed", "cancelled"].includes(job.status)) throw httpError(409, "job_not_runnable", "当前任务状态不能执行", { status: job.status });
|
||||
const unresolved = unresolvedDependencies(jobId);
|
||||
|
||||
+21
-4
@@ -141,6 +141,7 @@ import { createSsoTicket, handleOidcCallback, redeemSsoTicket, startOidcLogin }
|
||||
import { handleSamlCallback, isSamlProvider, samlServiceProviderMetadata, startSamlLogin } from "./saml.mjs";
|
||||
import { listWorkItems } from "./work-items.mjs";
|
||||
import { addTaskLink, createProjectTask, createTaskComment, getProjectTask, listProjectActivity, listProjectTasks, listTaskComments, removeTaskLink, updateProjectTask } from "./tasks.mjs";
|
||||
import { searchPlatform } from "./search.mjs";
|
||||
import { consumeRateLimit, rateLimitHeaders, rateLimitIdentity } from "./rate-limit.mjs";
|
||||
import { createDeliveryAccessLink, listDeliveryAccessFeedback, listDeliveryAccessLinks, readPublicDeliveryFile, resolvePublicDeliveryPortal, revokeDeliveryAccessLink, submitPublicDeliveryFeedback } from "./delivery-portal.mjs";
|
||||
|
||||
@@ -953,12 +954,17 @@ function createInvitation(context, organizationId, body) {
|
||||
throw httpError(409, "invitation_already_pending", "该邮箱已有待处理邀请,请重新发送或先撤销旧邀请", { email });
|
||||
}
|
||||
assertOrganizationSeatAvailable(organizationId, { email });
|
||||
const roleKey = body.roleKey || "writer";
|
||||
if (!dbGet("SELECT key FROM roles WHERE key = ?", [roleKey])) throw httpError(400, "role_invalid", "邀请角色不存在", { roleKey });
|
||||
const workspaceId = body.workspaceId || null;
|
||||
if (workspaceId && !dbGet("SELECT id FROM workspaces WHERE id = ? AND organization_id = ?", [workspaceId, organizationId])) throw httpError(400, "workspace_invalid", "邀请目标工作区不属于当前组织");
|
||||
const projectId = body.projectId || null;
|
||||
if (projectId && !workspaceId) throw httpError(400, "workspace_required_for_project_invitation", "项目级邀请必须同时指定所属工作区");
|
||||
if (projectId && !dbGet("SELECT id FROM projects WHERE id = ? AND workspace_id = ?", [projectId, workspaceId])) throw httpError(400, "project_invalid", "邀请目标项目不属于当前工作区");
|
||||
const invitationScope = projectId ? "project" : workspaceId ? "workspace" : "organization";
|
||||
const defaultRole = invitationScope === "project" ? "project_editor" : invitationScope === "workspace" ? "writer" : "org_member";
|
||||
const requestedRoleKey = String(body.roleKey || defaultRole).trim();
|
||||
const role = roleForScope(requestedRoleKey, invitationScope);
|
||||
if (invitationScope === "organization" && role.key === "org_owner") throw httpError(400, "owner_invitation_not_allowed", "组织所有者不能通过普通邀请授予,请在成员管理中完成所有者转移");
|
||||
const roleKey = role.key;
|
||||
const id = createId("invite");
|
||||
const inviteToken = `invite-${randomBytes(24).toString("base64url")}`;
|
||||
const tokenHash = createHash("sha256").update(inviteToken).digest("hex");
|
||||
@@ -1019,12 +1025,14 @@ function updateWorkspaceMember(context, workspaceId, userId, body) {
|
||||
const member = dbGet("SELECT * FROM workspace_members WHERE workspace_id = ? AND user_id = ?", [workspaceId, userId]);
|
||||
if (!member) throw httpError(404, "member_not_found", "工作区成员不存在");
|
||||
const roleKey = String(body.roleKey || member.role_key);
|
||||
const accessMode = String(body.accessMode || member.access_mode || "all");
|
||||
const status = String(body.status || member.status);
|
||||
roleForScope(roleKey, "workspace");
|
||||
if (!["all", "project-only"].includes(accessMode)) throw httpError(400, "workspace_access_mode_invalid", "工作区访问范围只能是 all 或 project-only");
|
||||
if (!["active", "suspended"].includes(status)) throw httpError(400, "member_status_invalid", "成员状态无效");
|
||||
const timestamp = new Date().toISOString();
|
||||
dbRun("UPDATE workspace_members SET role_key = ?, status = ?, updated_at = ? WHERE workspace_id = ? AND user_id = ?", [roleKey, status, timestamp, workspaceId, userId]);
|
||||
addAudit({ context, action: "workspace.member.updated", targetType: "workspace_member", targetId: `${workspaceId}:${userId}`, metadata: { previousRole: member.role_key, roleKey, previousStatus: member.status, status } });
|
||||
dbRun("UPDATE workspace_members SET role_key = ?, access_mode = ?, status = ?, updated_at = ? WHERE workspace_id = ? AND user_id = ?", [roleKey, accessMode, status, timestamp, workspaceId, userId]);
|
||||
addAudit({ context, action: "workspace.member.updated", targetType: "workspace_member", targetId: `${workspaceId}:${userId}`, metadata: { previousRole: member.role_key, roleKey, previousAccessMode: member.access_mode || "all", accessMode, previousStatus: member.status, status } });
|
||||
return { members: workspaceMembers(workspaceId) };
|
||||
}
|
||||
|
||||
@@ -1508,6 +1516,15 @@ createServer(async (req, res) => {
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/context") return send(res, 200, platformPayload(resolveContext(req.headers, url.searchParams)));
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/search") {
|
||||
const context = resolveContext(req.headers, url.searchParams);
|
||||
return send(res, 200, searchPlatform(context, {
|
||||
query: url.searchParams.get("q") || url.searchParams.get("query") || "",
|
||||
scope: url.searchParams.get("scope") || "workspace",
|
||||
limit: url.searchParams.get("limit") || 40
|
||||
}));
|
||||
}
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/work-items") {
|
||||
const context = resolveContext(req.headers, url.searchParams);
|
||||
return send(res, 200, listWorkItems(context, { limit: url.searchParams.get("limit") }));
|
||||
|
||||
@@ -290,6 +290,7 @@ CREATE TABLE IF NOT EXISTS workspace_members (
|
||||
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role_key TEXT NOT NULL,
|
||||
access_mode TEXT NOT NULL DEFAULT 'all',
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'invited', 'suspended')),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { dbAll } from "./db.mjs";
|
||||
|
||||
const SEARCH_LIMIT = 60;
|
||||
|
||||
function placeholders(values) {
|
||||
return values.map(() => "?").join(",");
|
||||
}
|
||||
|
||||
function likePattern(value) {
|
||||
return `%${String(value || "").trim().replace(/[\\%_]/g, "\\$&").slice(0, 80)}%`;
|
||||
}
|
||||
|
||||
function scopeProjects(context, scope) {
|
||||
const accessible = Array.isArray(context?.projects) ? context.projects : [];
|
||||
if (scope === "project" && context?.project?.id) return [context.project.id];
|
||||
return accessible.map((project) => project.id).filter(Boolean);
|
||||
}
|
||||
|
||||
function baseResult(type, typeLabel, row, targetTab, title, subtitle, status = "") {
|
||||
return {
|
||||
type,
|
||||
typeLabel,
|
||||
id: row.id,
|
||||
title: String(title || row.name || row.title || row.id),
|
||||
subtitle: String(subtitle || ""),
|
||||
status: String(status || row.status || ""),
|
||||
updatedAt: row.updated_at || row.updatedAt || row.created_at || "",
|
||||
targetTab,
|
||||
organizationId: row.organization_id || "",
|
||||
workspaceId: row.workspace_id || "",
|
||||
projectId: row.project_id || row.projectId || (type === "project" ? row.id : "")
|
||||
};
|
||||
}
|
||||
|
||||
function sortResults(results) {
|
||||
return results.sort((left, right) => {
|
||||
const leftTitle = left.title.toLocaleLowerCase();
|
||||
const rightTitle = right.title.toLocaleLowerCase();
|
||||
const leftStarts = leftTitle.startsWith(left.query) ? 1 : 0;
|
||||
const rightStarts = rightTitle.startsWith(right.query) ? 1 : 0;
|
||||
if (leftStarts !== rightStarts) return rightStarts - leftStarts;
|
||||
return String(right.updatedAt).localeCompare(String(left.updatedAt));
|
||||
});
|
||||
}
|
||||
|
||||
export function searchPlatform(context, { query = "", scope = "workspace", limit = 40 } = {}) {
|
||||
const normalizedQuery = String(query || "").trim().slice(0, 80);
|
||||
if (!normalizedQuery) return { query: "", scope, total: 0, results: [] };
|
||||
|
||||
const projectIds = scopeProjects(context, scope === "project" ? "project" : "workspace");
|
||||
if (!projectIds.length) return { query: normalizedQuery, scope, total: 0, results: [] };
|
||||
|
||||
const safeLimit = Math.min(SEARCH_LIMIT, Math.max(1, Number(limit || 40)));
|
||||
const perType = Math.min(12, Math.max(4, Math.ceil(safeLimit / 8) + 2));
|
||||
const ids = placeholders(projectIds);
|
||||
const pattern = likePattern(normalizedQuery);
|
||||
const results = [];
|
||||
|
||||
const projects = dbAll(
|
||||
`SELECT p.*, w.name AS workspace_name, w.organization_id AS organization_id
|
||||
FROM projects p
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.id IN (${ids})
|
||||
AND (p.name LIKE ? ESCAPE '\\' OR p.type LIKE ? ESCAPE '\\')
|
||||
ORDER BY p.updated_at DESC LIMIT ?`,
|
||||
[...projectIds, pattern, pattern, perType]
|
||||
);
|
||||
for (const row of projects) {
|
||||
results.push(baseResult("project", "项目", row, "factory", row.name, `${row.workspace_name} · ${row.type}`, row.status));
|
||||
}
|
||||
|
||||
const episodes = dbAll(
|
||||
`SELECT e.*, se.season_number, se.title AS season_title, se.series_id,
|
||||
p.id AS project_id, p.name AS project_name, w.organization_id AS organization_id, p.workspace_id, w.name AS workspace_name
|
||||
FROM episodes e
|
||||
JOIN seasons se ON se.id = e.season_id
|
||||
JOIN series s ON s.id = se.series_id
|
||||
JOIN projects p ON p.id = s.project_id
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.id IN (${ids})
|
||||
AND (e.title LIKE ? ESCAPE '\\' OR e.hook LIKE ? ESCAPE '\\' OR e.cliffhanger LIKE ? ESCAPE '\\')
|
||||
ORDER BY e.updated_at DESC LIMIT ?`,
|
||||
[...projectIds, pattern, pattern, pattern, perType]
|
||||
);
|
||||
for (const row of episodes) {
|
||||
results.push(baseResult("episode", "分集", row, "bible", row.title, `${row.project_name} · 第 ${row.episode_number} 集 · ${row.season_title}`, row.status));
|
||||
}
|
||||
|
||||
const documents = dbAll(
|
||||
`SELECT d.*, e.episode_number, e.title AS episode_title,
|
||||
p.name AS project_name, w.organization_id AS organization_id, p.workspace_id
|
||||
FROM script_documents d
|
||||
LEFT JOIN episodes e ON e.id = d.episode_id
|
||||
JOIN projects p ON p.id = d.project_id
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.id IN (${ids})
|
||||
AND (d.title LIKE ? ESCAPE '\\' OR d.content LIKE ? ESCAPE '\\')
|
||||
ORDER BY d.updated_at DESC LIMIT ?`,
|
||||
[...projectIds, pattern, pattern, perType]
|
||||
);
|
||||
for (const row of documents) {
|
||||
results.push(baseResult("script", "剧本", row, "script", row.title, `${row.project_name}${row.episode_title ? ` · ${row.episode_title}` : ""} · v${row.version_number}`, row.status));
|
||||
}
|
||||
|
||||
const shots = dbAll(
|
||||
`SELECT sh.*, e.episode_number, e.title AS episode_title,
|
||||
p.id AS project_id, p.name AS project_name, w.organization_id AS organization_id, p.workspace_id
|
||||
FROM shots sh
|
||||
JOIN episodes e ON e.id = sh.episode_id
|
||||
JOIN seasons se ON se.id = e.season_id
|
||||
JOIN series s ON s.id = se.series_id
|
||||
JOIN projects p ON p.id = s.project_id
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.id IN (${ids})
|
||||
AND (sh.title LIKE ? ESCAPE '\\' OR sh.id LIKE ? ESCAPE '\\' OR sh.continuity_json LIKE ? ESCAPE '\\')
|
||||
ORDER BY sh.updated_at DESC LIMIT ?`,
|
||||
[...projectIds, pattern, pattern, pattern, perType]
|
||||
);
|
||||
for (const row of shots) {
|
||||
results.push(baseResult("shot", "镜头", row, "director", row.title, `${row.project_name} · E${String(row.episode_number).padStart(2, "0")} · S${String(row.shot_number).padStart(2, "0")}`, row.status));
|
||||
}
|
||||
|
||||
const assets = dbAll(
|
||||
`SELECT a.*, p.id AS project_id, p.name AS project_name, w.organization_id AS organization_id, p.workspace_id
|
||||
FROM assets a
|
||||
JOIN projects p ON p.id = a.project_id
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.id IN (${ids})
|
||||
AND (a.name LIKE ? ESCAPE '\\' OR a.kind LIKE ? ESCAPE '\\')
|
||||
ORDER BY a.updated_at DESC LIMIT ?`,
|
||||
[...projectIds, pattern, pattern, perType]
|
||||
);
|
||||
for (const row of assets) {
|
||||
results.push(baseResult("asset", "资产", row, "casting", row.name, `${row.project_name} · ${row.kind}`, row.lock_status));
|
||||
}
|
||||
|
||||
const tasks = dbAll(
|
||||
`SELECT t.*, p.id AS project_id, p.name AS project_name, w.organization_id AS organization_id, p.workspace_id,
|
||||
u.display_name AS assignee_name
|
||||
FROM project_tasks t
|
||||
JOIN projects p ON p.id = t.project_id
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
LEFT JOIN users u ON u.id = t.assignee_user_id
|
||||
WHERE p.id IN (${ids})
|
||||
AND (t.title LIKE ? ESCAPE '\\' OR t.description LIKE ? ESCAPE '\\')
|
||||
ORDER BY t.updated_at DESC LIMIT ?`,
|
||||
[...projectIds, pattern, pattern, perType]
|
||||
);
|
||||
for (const row of tasks) {
|
||||
results.push(baseResult("task", "任务", row, "tasks", row.title, `${row.project_name} · ${row.assignee_name || "未分派"}`, row.status));
|
||||
}
|
||||
|
||||
const jobs = dbAll(
|
||||
`SELECT j.*, p.name AS project_name, w.organization_id AS organization_id
|
||||
FROM generation_jobs j
|
||||
JOIN projects p ON p.id = j.project_id
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.id IN (${ids})
|
||||
AND (j.kind LIKE ? ESCAPE '\\' OR j.status LIKE ? ESCAPE '\\' OR j.id LIKE ? ESCAPE '\\' OR j.error_message LIKE ? ESCAPE '\\')
|
||||
ORDER BY j.updated_at DESC LIMIT ?`,
|
||||
[...projectIds, pattern, pattern, pattern, pattern, perType]
|
||||
);
|
||||
for (const row of jobs) {
|
||||
results.push(baseResult("job", "生成任务", row, "jobs", row.kind || row.id, `${row.project_name} · ${row.id}`, row.status));
|
||||
}
|
||||
|
||||
const deliveries = dbAll(
|
||||
`SELECT d.*, p.name AS project_name, w.organization_id AS organization_id
|
||||
FROM deliveries d
|
||||
JOIN projects p ON p.id = d.project_id
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.id IN (${ids})
|
||||
AND (d.version LIKE ? ESCAPE '\\' OR d.channel LIKE ? ESCAPE '\\' OR d.status LIKE ? ESCAPE '\\' OR d.id LIKE ? ESCAPE '\\')
|
||||
ORDER BY d.updated_at DESC LIMIT ?`,
|
||||
[...projectIds, pattern, pattern, pattern, pattern, perType]
|
||||
);
|
||||
for (const row of deliveries) {
|
||||
results.push(baseResult("delivery", "交付", row, "export", `${row.project_name} · ${row.version}`, `${row.channel} · ${row.id}`, row.status));
|
||||
}
|
||||
|
||||
const ranked = sortResults(results.map((result) => ({ ...result, query: normalizedQuery.toLocaleLowerCase() })));
|
||||
return {
|
||||
query: normalizedQuery,
|
||||
scope: scope === "project" && context?.project?.id ? "project" : "workspace",
|
||||
total: ranked.length,
|
||||
results: ranked.slice(0, safeLimit).map(({ query: _query, ...result }) => result)
|
||||
};
|
||||
}
|
||||
+15
-8
@@ -114,7 +114,8 @@ function accessibleProjects(workspaceId, userId, organizationId) {
|
||||
FROM projects p
|
||||
LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = ? AND pm.status = 'active'
|
||||
JOIN workspace_members wm ON wm.workspace_id = p.workspace_id AND wm.user_id = ? AND wm.status = 'active'
|
||||
WHERE p.workspace_id = ? AND (pm.user_id IS NOT NULL OR wm.user_id IS NOT NULL)
|
||||
WHERE p.workspace_id = ?
|
||||
AND (wm.access_mode IS NULL OR wm.access_mode = 'all' OR (wm.access_mode = 'project-only' AND pm.user_id IS NOT NULL))
|
||||
ORDER BY CASE WHEN p.status = 'archived' THEN 1 ELSE 0 END, p.updated_at DESC`,
|
||||
[userId, userId, workspaceId]
|
||||
);
|
||||
@@ -179,7 +180,8 @@ function ensureProject(userId, organization, workspace, projectId) {
|
||||
[projectId, userId]
|
||||
);
|
||||
const workspaceMember = dbGet("SELECT * FROM workspace_members WHERE workspace_id = ? AND user_id = ? AND status = 'active'", [workspace.id, userId]);
|
||||
if (!membership && !workspaceMember && !canSeeAllWorkspaceData(organization.id, userId)) {
|
||||
const workspaceCanSeeProject = workspaceMember && (workspaceMember.access_mode !== "project-only" || Boolean(membership));
|
||||
if (!membership && !workspaceCanSeeProject && !canSeeAllWorkspaceData(organization.id, userId)) {
|
||||
throw httpError(403, "project_forbidden", "当前用户没有该项目访问权", { projectId, userId });
|
||||
}
|
||||
return { project, membership };
|
||||
@@ -304,7 +306,7 @@ export function orgMembers(organizationId) {
|
||||
|
||||
export function workspaceMembers(workspaceId) {
|
||||
return dbAll(
|
||||
`SELECT wm.id, wm.user_id, u.display_name, u.email, u.avatar_color, wm.role_key, r.name AS role_name, wm.status
|
||||
`SELECT wm.id, wm.user_id, u.display_name, u.email, u.avatar_color, wm.role_key, r.name AS role_name, wm.access_mode, wm.status
|
||||
FROM workspace_members wm
|
||||
JOIN users u ON u.id = wm.user_id
|
||||
LEFT JOIN roles r ON r.key = wm.role_key
|
||||
@@ -486,7 +488,10 @@ export function previewInvitation(token) {
|
||||
}
|
||||
|
||||
function invitationRoleForWorkspace(roleKey) {
|
||||
return roleKey === "writer" || roleKey === "voice_editor" || roleKey === "art_director" ? roleKey : "producer";
|
||||
if (["producer", "writer", "art_director", "voice_editor", "reviewer"].includes(roleKey)) return roleKey;
|
||||
// Project invitations need a workspace membership for context resolution,
|
||||
// but must not inherit workspace-wide production permissions.
|
||||
return "project_guest";
|
||||
}
|
||||
|
||||
function applyInvitationMembership(invitation, userId, timestamp) {
|
||||
@@ -497,15 +502,17 @@ function applyInvitationMembership(invitation, userId, timestamp) {
|
||||
[`om-${invitation.organization_id}-${userId}`, invitation.organization_id, userId, organizationRole, timestamp, timestamp, timestamp]
|
||||
);
|
||||
if (invitation.workspace_id) {
|
||||
const accessMode = invitation.project_id ? "project-only" : "all";
|
||||
dbRun(
|
||||
"INSERT INTO workspace_members(id, workspace_id, user_id, role_key, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, ?) ON CONFLICT(workspace_id, user_id) DO UPDATE SET role_key = excluded.role_key, status = 'active', updated_at = excluded.updated_at",
|
||||
[`wm-${invitation.workspace_id}-${userId}`, invitation.workspace_id, userId, invitationRoleForWorkspace(invitation.role_key), timestamp, timestamp]
|
||||
"INSERT INTO workspace_members(id, workspace_id, user_id, role_key, access_mode, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'active', ?, ?) ON CONFLICT(workspace_id, user_id) DO UPDATE SET role_key = excluded.role_key, access_mode = CASE WHEN workspace_members.access_mode = 'all' THEN 'all' ELSE excluded.access_mode END, status = 'active', updated_at = excluded.updated_at",
|
||||
[`wm-${invitation.workspace_id}-${userId}`, invitation.workspace_id, userId, invitationRoleForWorkspace(invitation.role_key), accessMode, timestamp, timestamp]
|
||||
);
|
||||
}
|
||||
if (invitation.project_id) {
|
||||
const projectRole = ["project_editor", "project_viewer"].includes(invitation.role_key) ? invitation.role_key : "project_editor";
|
||||
dbRun(
|
||||
"INSERT INTO project_members(id, project_id, user_id, role_key, status, created_at, updated_at) VALUES (?, ?, ?, 'project_editor', 'active', ?, ?) ON CONFLICT(project_id, user_id) DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
|
||||
[`pm-${invitation.project_id}-${userId}`, invitation.project_id, userId, timestamp, timestamp]
|
||||
"INSERT INTO project_members(id, project_id, user_id, role_key, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, ?) ON CONFLICT(project_id, user_id) DO UPDATE SET role_key = excluded.role_key, status = 'active', updated_at = excluded.updated_at",
|
||||
[`pm-${invitation.project_id}-${userId}`, invitation.project_id, userId, projectRole, timestamp, timestamp]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user