SECURITY - LAUNCHER: harden storage and access lifecycle
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { mkdir, rename, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
const collectionKeys = [
|
||||
"clients",
|
||||
@@ -62,16 +62,18 @@ const defaultSettings = {
|
||||
};
|
||||
|
||||
export function createControlPlaneStore({ projectRoot }) {
|
||||
const publicStorageRoot = join(projectRoot, "public", "storage");
|
||||
const distStorageRoot = join(projectRoot, "dist", "storage");
|
||||
const dataPath = join(publicStorageRoot, "launcher-data.json");
|
||||
const serverStorageRoot = resolveStorageRoot(projectRoot);
|
||||
const legacyPublicDataPath = join(projectRoot, "public", "storage", "launcher-data.json");
|
||||
const dataPath = join(serverStorageRoot, "launcher-data.json");
|
||||
|
||||
function readData() {
|
||||
if (!existsSync(dataPath)) {
|
||||
const readablePath = existsSync(dataPath) ? dataPath : legacyPublicDataPath;
|
||||
|
||||
if (!existsSync(readablePath)) {
|
||||
return normalizeData({});
|
||||
}
|
||||
|
||||
return normalizeData(JSON.parse(readFileSync(dataPath, "utf8")));
|
||||
return normalizeData(JSON.parse(readFileSync(readablePath, "utf8")));
|
||||
}
|
||||
|
||||
async function writeData(data) {
|
||||
@@ -1192,6 +1194,10 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||
throw new Error("Этот инвайт выписан на другую почту");
|
||||
}
|
||||
|
||||
if (invite.status === "accepted") {
|
||||
throw new Error("Инвайт уже принят");
|
||||
}
|
||||
|
||||
if (invite.status === "revoked") {
|
||||
throw new Error("Инвайт отозван");
|
||||
}
|
||||
@@ -1711,13 +1717,7 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||
}
|
||||
|
||||
function getWritableStorageRoots() {
|
||||
const roots = [publicStorageRoot];
|
||||
|
||||
if (existsSync(join(projectRoot, "dist"))) {
|
||||
roots.push(distStorageRoot);
|
||||
}
|
||||
|
||||
return roots;
|
||||
return [serverStorageRoot];
|
||||
}
|
||||
|
||||
function getLoginAccountStatus(email) {
|
||||
@@ -1799,6 +1799,16 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveStorageRoot(projectRoot) {
|
||||
const configuredRoot = process.env.NODEDC_LAUNCHER_STORAGE_DIR;
|
||||
|
||||
if (configuredRoot && configuredRoot.trim()) {
|
||||
return isAbsolute(configuredRoot) ? configuredRoot : resolve(projectRoot, configuredRoot);
|
||||
}
|
||||
|
||||
return join(projectRoot, "server", "storage");
|
||||
}
|
||||
|
||||
function normalizeData(payload) {
|
||||
const data = typeof payload === "object" && payload !== null ? { ...payload } : {};
|
||||
|
||||
|
||||
+307
-33
@@ -249,7 +249,14 @@ app.get("/api/me", (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeContext = getRuntimeSessionContext(session);
|
||||
const sessionAccess = getSessionAccessState(session);
|
||||
|
||||
if (!sessionAccess.ok) {
|
||||
rejectInactiveSession(res, session, sessionAccess);
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeContext = sessionAccess.runtimeContext;
|
||||
|
||||
res.json({
|
||||
authenticated: true,
|
||||
@@ -268,7 +275,14 @@ app.get("/api/apps", (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ apps: getAppsForSession(session) });
|
||||
const sessionAccess = getSessionAccessState(session);
|
||||
|
||||
if (!sessionAccess.ok) {
|
||||
rejectInactiveSession(res, session, sessionAccess);
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ apps: getAppsForUser(sessionAccess.runtimeContext.groups) });
|
||||
});
|
||||
|
||||
app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => {
|
||||
@@ -456,9 +470,20 @@ app.post("/api/internal/tasker/invite-requests", asyncRoute(async (req, res) =>
|
||||
|
||||
const groups = resolveRequiredGroups(snapshot.data, inviter);
|
||||
const app = getAppsForUser(groups).find((candidate) => candidate.slug === "task-manager");
|
||||
const workspacePolicy = resolveTaskManagerWorkspacePolicy(snapshot.data, groups, Boolean(app?.hasAccess), inviter);
|
||||
const workspaceSlug = req.body?.workspace?.slug ?? req.body?.workspaceSlug;
|
||||
const workspacePolicy = resolveTaskManagerWorkspacePolicy(
|
||||
snapshot.data,
|
||||
groups,
|
||||
Boolean(app?.hasAccess),
|
||||
inviter,
|
||||
workspaceSlug
|
||||
);
|
||||
|
||||
if (!app?.hasAccess || workspacePolicy.inviteApproval !== "nodedc") {
|
||||
if (
|
||||
!app?.hasAccess ||
|
||||
workspacePolicy.managedBy !== "tasker" ||
|
||||
!["nodedc", "launcher"].includes(workspacePolicy.inviteApproval)
|
||||
) {
|
||||
res.status(403).json({ ok: false, error: "nodedc_tasker_invite_approval_not_allowed", workspacePolicy });
|
||||
return;
|
||||
}
|
||||
@@ -518,9 +543,10 @@ app.patch("/api/profile", requireSession, asyncRoute(async (req, res) => {
|
||||
userId: actor.id,
|
||||
});
|
||||
const storeResult = await controlPlaneStore.markUserAuthentikProvisioned(actor.id, provisionedUser, req.nodedcSession.user);
|
||||
const taskManagerProfile = await syncTaskManagerUserProfile(storeResult.user);
|
||||
|
||||
publishControlPlaneEvent("profile.updated", [actor.id]);
|
||||
res.json({ ...storeResult, provisioning: toProvisioningResponse(provisionedUser) });
|
||||
res.json({ ...storeResult, provisioning: toProvisioningResponse(provisionedUser), taskManagerProfile });
|
||||
}));
|
||||
|
||||
app.post("/api/profile/password", requireSession, asyncRoute(async (req, res) => {
|
||||
@@ -629,7 +655,15 @@ app.get("/tasker-workspace-invite/:taskerInviteRequestId", (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeContext = getRuntimeSessionContext(session);
|
||||
const sessionAccess = getSessionAccessState(session);
|
||||
|
||||
if (!sessionAccess.ok) {
|
||||
expireSession(res, session);
|
||||
res.redirect(buildLoginRedirectUrl(req.originalUrl, { forceLogin: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeContext = sessionAccess.runtimeContext;
|
||||
const request = controlPlaneStore
|
||||
.getSnapshot({ name: "NODE.DC tasker invite redirect" })
|
||||
.data.taskerInviteRequests.find((candidate) => candidate.id === req.params.taskerInviteRequestId);
|
||||
@@ -660,6 +694,25 @@ app.get("/api/admin/control-plane", requireLauncherAdmin, (req, res) => {
|
||||
res.json(scopeAdminSnapshot(req));
|
||||
});
|
||||
|
||||
app.get("/api/storage/data", requireSession, (req, res) => {
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
|
||||
if (snapshot.actor.source !== "launcher") {
|
||||
res.status(403).json({ error: "Профиль пользователя не найден в Launcher control-plane" });
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeContext = getRuntimeSessionContext(req.nodedcSession);
|
||||
const adminScope = resolveAdminScope(runtimeContext.user, runtimeContext.groups);
|
||||
|
||||
if (adminScope.isRoot || adminScope.clientIds.size > 0) {
|
||||
res.json(scopeControlPlaneData(snapshot.data, adminScope));
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(scopeRuntimeControlPlaneData(snapshot.data, snapshot.actor.id));
|
||||
});
|
||||
|
||||
app.patch("/api/admin/settings", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.updateSettings(req.body, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.settings.updated");
|
||||
@@ -1019,8 +1072,10 @@ app.patch("/api/admin/users/:userId/profile", requireLauncherAdmin, asyncRoute(a
|
||||
|
||||
const result = await controlPlaneStore.updateUserProfile(req.params.userId, req.body, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(result.data, [req.params.userId], req.nodedcSession.user);
|
||||
const updatedUser = syncResult.data.users.find((candidate) => candidate.id === req.params.userId);
|
||||
const taskManagerProfile = await syncTaskManagerUserProfile(updatedUser);
|
||||
publishControlPlaneEvent("admin.user.updated", syncResult.userIds);
|
||||
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
res.json({ ...scopeAdminMutationResult(req, { ...result, data: syncResult.data }), taskManagerProfile });
|
||||
}));
|
||||
|
||||
app.delete("/api/admin/users/:userId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
@@ -1038,9 +1093,10 @@ app.delete("/api/admin/users/:userId", requireLauncherAdmin, requireRootLauncher
|
||||
authentik = await authentikSyncClient.deleteUser({ data: snapshot.data, userId: req.params.userId });
|
||||
}
|
||||
|
||||
const taskManagerCleanup = await cleanupTaskManagerUserAccess(user);
|
||||
const result = await controlPlaneStore.deleteUser(req.params.userId, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.user.deleted", [req.params.userId]);
|
||||
res.json({ ...scopeAdminMutationResult(req, result), authentik });
|
||||
res.json({ ...scopeAdminMutationResult(req, result), authentik, taskManagerCleanup });
|
||||
}));
|
||||
|
||||
app.post("/api/admin/users/:userId/provision-authentik", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
@@ -1371,7 +1427,7 @@ app.get("/api/admin/sync/authentik/plan", requireLauncherAdmin, requireRootLaunc
|
||||
res.json(controlPlaneStore.buildAuthentikSyncPlan());
|
||||
});
|
||||
|
||||
app.post("/api/storage/upload", asyncRoute(async (req, res) => {
|
||||
app.post("/api/storage/upload", requireSession, asyncRoute(async (req, res) => {
|
||||
const result = await saveUploadedFile(req.body);
|
||||
res.json(result);
|
||||
}));
|
||||
@@ -1379,9 +1435,14 @@ app.post("/api/storage/upload", asyncRoute(async (req, res) => {
|
||||
app.post("/api/storage/data", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
await saveLauncherData(req.body);
|
||||
publishControlPlaneEvent("storage.data.updated");
|
||||
res.json({ ok: true, url: "/storage/launcher-data.json" });
|
||||
res.json({ ok: true });
|
||||
}));
|
||||
|
||||
app.get("/storage/launcher-data.json", (_req, res) => {
|
||||
setNoStore(res);
|
||||
res.status(404).json({ error: "not_found" });
|
||||
});
|
||||
|
||||
const vite = await createViteServer({
|
||||
root: projectRoot,
|
||||
appType: "spa",
|
||||
@@ -1433,7 +1494,6 @@ function readConfig() {
|
||||
internalAccessToken:
|
||||
process.env.NODEDC_INTERNAL_ACCESS_TOKEN ??
|
||||
process.env.NODEDC_PLATFORM_SERVICE_TOKEN ??
|
||||
process.env.PLANE_OIDC_CLIENT_SECRET ??
|
||||
"",
|
||||
taskLogoutUrl:
|
||||
process.env.TASK_LOGOUT_URL ??
|
||||
@@ -1771,26 +1831,78 @@ function getRuntimeSessionContext(session) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const groups = resolveRequiredGroups(snapshot.data, user);
|
||||
|
||||
return {
|
||||
groups,
|
||||
user: {
|
||||
...session.user,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatarUrl: user.avatarUrl ?? session.user.avatarUrl,
|
||||
groups,
|
||||
},
|
||||
};
|
||||
return buildRuntimeSessionContext(session, snapshot.data, user);
|
||||
} catch (error) {
|
||||
console.warn(error instanceof Error ? error.message : "Не удалось рассчитать runtime контекст Launcher");
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function getAppsForSession(session) {
|
||||
return getAppsForUser(getRuntimeSessionContext(session).groups);
|
||||
function buildRuntimeSessionContext(session, data, user) {
|
||||
const groups = resolveRequiredGroups(data, user);
|
||||
|
||||
return {
|
||||
groups,
|
||||
user: {
|
||||
...session.user,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatarUrl: user.avatarUrl ?? session.user.avatarUrl,
|
||||
groups,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getSessionAccessState(session) {
|
||||
try {
|
||||
const snapshot = controlPlaneStore.getSnapshot(session.user);
|
||||
const sessionEmail = typeof session.user?.email === "string" ? session.user.email.toLowerCase() : "";
|
||||
|
||||
if (snapshot.actor.source !== "launcher") {
|
||||
const revokedAccount = snapshot.data.revokedAccounts.find((account) => account.email.toLowerCase() === sessionEmail);
|
||||
return {
|
||||
ok: false,
|
||||
status: 401,
|
||||
error: revokedAccount ? "account_revoked" : "account_not_found",
|
||||
message: revokedAccount
|
||||
? "Аккаунт больше не активен. Запросите доступ, если хотите подключиться снова."
|
||||
: "Профиль пользователя не найден в Launcher control-plane.",
|
||||
};
|
||||
}
|
||||
|
||||
const user = snapshot.data.users.find((candidate) => candidate.id === snapshot.actor.id);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 401,
|
||||
error: "account_not_found",
|
||||
message: "Профиль пользователя не найден в Launcher control-plane.",
|
||||
};
|
||||
}
|
||||
|
||||
if (user.globalStatus === "blocked") {
|
||||
return {
|
||||
ok: false,
|
||||
status: 401,
|
||||
error: "account_blocked",
|
||||
message: "Аккаунт заблокирован. Обратитесь к администратору NODE.DC.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
runtimeContext: buildRuntimeSessionContext(session, snapshot.data, user),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(error instanceof Error ? error.message : "Не удалось проверить Launcher-сессию");
|
||||
return {
|
||||
ok: false,
|
||||
status: 401,
|
||||
error: "session_invalid",
|
||||
message: "Не удалось проверить Launcher-сессию.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getAppsForUser(userGroups) {
|
||||
@@ -1809,7 +1921,7 @@ function getAppsForUser(userGroups) {
|
||||
hasAccess,
|
||||
accessReason: hasAccess ? "Доступ подтверждён" : "Нет доступа",
|
||||
};
|
||||
});
|
||||
}).filter((app) => app.hasAccess);
|
||||
}
|
||||
|
||||
function getAppCatalog() {
|
||||
@@ -1904,6 +2016,54 @@ async function requestTaskManagerInternalJson(pathname, init = {}) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function syncTaskManagerUserProfile(user) {
|
||||
if (!user?.email || !config.internalAccessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await requestTaskManagerInternalJson("/api/internal/nodedc/users/profile-sync/", {
|
||||
method: "POST",
|
||||
body: {
|
||||
email: user.email,
|
||||
subject: user.authentikUserId ?? undefined,
|
||||
name: user.name,
|
||||
avatarUrl: resolveUserAvatarPublicUrl(user),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
error instanceof Error
|
||||
? `Task Manager profile sync failed: ${error.message}`
|
||||
: "Task Manager profile sync failed"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupTaskManagerUserAccess(user) {
|
||||
if (!user?.email || !config.internalAccessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await requestTaskManagerInternalJson("/api/internal/nodedc/logout/", {
|
||||
method: "POST",
|
||||
body: {
|
||||
source: "launcher-user-hard-delete",
|
||||
subject: user.authentikUserId ?? undefined,
|
||||
email: user.email,
|
||||
revokeIdentityLinks: true,
|
||||
revokeTaskerAccess: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Task Manager user cleanup failed";
|
||||
console.warn(`Task Manager user cleanup failed: ${message}`);
|
||||
return { ok: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonResponse(text, url) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
@@ -2037,7 +2197,7 @@ function pruneExpiredServiceHandoffs() {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess, user) {
|
||||
function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess, user, workspaceSlug = null) {
|
||||
const mode = data.settings?.taskManager?.workspaceCreationPolicy ?? "any_authorized_user";
|
||||
const groupSet = new Set(groups);
|
||||
const isSuperAdmin = groupSet.has("nodedc:superadmin");
|
||||
@@ -2049,6 +2209,10 @@ function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess, u
|
||||
);
|
||||
const defaultManagedBy = hasLauncherManagedWorkspace && !isSuperAdmin ? "launcher" : "tasker";
|
||||
const defaultInviteApproval = hasLauncherManagedWorkspace && !isSuperAdmin ? "launcher" : isPublicPoolUser ? "nodedc" : "tasker";
|
||||
const workspaceAssignment =
|
||||
typeof workspaceSlug === "string" && workspaceSlug.trim()
|
||||
? workspaces.find((workspace) => workspace.slug === workspaceSlug.trim())
|
||||
: null;
|
||||
|
||||
if (!hasTaskManagerAccess) {
|
||||
return {
|
||||
@@ -2077,6 +2241,32 @@ function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess, u
|
||||
}
|
||||
|
||||
if (hasLauncherManagedWorkspace && !isSuperAdmin) {
|
||||
if (workspaceAssignment?.managedBy === "launcher") {
|
||||
return {
|
||||
mode,
|
||||
managedBy: "launcher",
|
||||
defaultManagedBy: "launcher",
|
||||
inviteApproval: "launcher",
|
||||
defaultInviteApproval: "launcher",
|
||||
workspaces,
|
||||
canCreateWorkspace: false,
|
||||
reason: "Рабочие пространства этого пользователя управляются через Launcher.",
|
||||
};
|
||||
}
|
||||
|
||||
if (workspaceSlug) {
|
||||
return {
|
||||
mode,
|
||||
managedBy: "tasker",
|
||||
defaultManagedBy: "launcher",
|
||||
inviteApproval: defaultInviteApproval,
|
||||
defaultInviteApproval,
|
||||
workspaces,
|
||||
canCreateWorkspace: false,
|
||||
reason: "Self-service workspace работает через Tasker, approve инвайтов выполняет Launcher.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode,
|
||||
managedBy: "launcher",
|
||||
@@ -2306,11 +2496,8 @@ function getSessionSyncAllowedOrigins() {
|
||||
}
|
||||
|
||||
function readLauncherData() {
|
||||
const dataPath = join(projectRoot, "public", "storage", "launcher-data.json");
|
||||
|
||||
try {
|
||||
if (!existsSync(dataPath)) return null;
|
||||
return JSON.parse(readFileSync(dataPath, "utf8"));
|
||||
return controlPlaneStore.readData();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -2433,6 +2620,21 @@ function getCurrentSession(req) {
|
||||
return session;
|
||||
}
|
||||
|
||||
function expireSession(res, session) {
|
||||
sessions.delete(session.id);
|
||||
res.clearCookie(sessionCookieName, clearCookieOptions());
|
||||
}
|
||||
|
||||
function rejectInactiveSession(res, session, sessionAccess) {
|
||||
expireSession(res, session);
|
||||
res.status(sessionAccess.status ?? 401).json({
|
||||
authenticated: false,
|
||||
loginUrl: "/auth/login",
|
||||
error: sessionAccess.error,
|
||||
message: sessionAccess.message,
|
||||
});
|
||||
}
|
||||
|
||||
function pruneExpiredSessions() {
|
||||
for (const [sessionId, session] of sessions) {
|
||||
if (session.expiresAt < Date.now()) {
|
||||
@@ -2501,7 +2703,14 @@ function requireLauncherAdmin(req, res, next) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeContext = getRuntimeSessionContext(session);
|
||||
const sessionAccess = getSessionAccessState(session);
|
||||
|
||||
if (!sessionAccess.ok) {
|
||||
rejectInactiveSession(res, session, sessionAccess);
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeContext = sessionAccess.runtimeContext;
|
||||
const adminScope = resolveAdminScope(runtimeContext.user, runtimeContext.groups);
|
||||
|
||||
if (!adminScope.isRoot && adminScope.clientIds.size === 0) {
|
||||
@@ -2531,7 +2740,14 @@ function requireSession(req, res, next) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeContext = getRuntimeSessionContext(session);
|
||||
const sessionAccess = getSessionAccessState(session);
|
||||
|
||||
if (!sessionAccess.ok) {
|
||||
rejectInactiveSession(res, session, sessionAccess);
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeContext = sessionAccess.runtimeContext;
|
||||
req.nodedcSession = { ...session, user: runtimeContext.user };
|
||||
next();
|
||||
}
|
||||
@@ -2761,6 +2977,64 @@ function scopeControlPlaneData(data, scope) {
|
||||
};
|
||||
}
|
||||
|
||||
function scopeRuntimeControlPlaneData(data, userId) {
|
||||
const user = data.users.find((candidate) => candidate.id === userId);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
...data,
|
||||
clients: [],
|
||||
users: [],
|
||||
memberships: [],
|
||||
groups: [],
|
||||
invites: [],
|
||||
accessRequests: [],
|
||||
revokedAccounts: [],
|
||||
taskerInviteRequests: [],
|
||||
grants: [],
|
||||
exceptions: [],
|
||||
syncStatuses: [],
|
||||
auditEvents: [],
|
||||
taskManagerMemberships: [],
|
||||
taskManagerProjectMemberships: [],
|
||||
};
|
||||
}
|
||||
|
||||
const memberships = data.memberships.filter((membership) => membership.userId === user.id);
|
||||
const clientIds = new Set(memberships.map((membership) => membership.clientId));
|
||||
const groups = data.groups
|
||||
.filter((group) => clientIds.has(group.clientId) && group.memberIds.includes(user.id))
|
||||
.map((group) => ({ ...group, memberIds: group.memberIds.filter((memberId) => memberId === user.id) }));
|
||||
const groupIds = new Set(groups.map((group) => group.id));
|
||||
|
||||
return {
|
||||
...data,
|
||||
clients: data.clients.filter((client) => clientIds.has(client.id)),
|
||||
users: [user],
|
||||
memberships,
|
||||
groups,
|
||||
invites: [],
|
||||
accessRequests: [],
|
||||
revokedAccounts: [],
|
||||
taskerInviteRequests: [],
|
||||
grants: data.grants.filter((grant) => {
|
||||
if (grant.targetType === "client") return clientIds.has(grant.targetId);
|
||||
if (grant.targetType === "group") return groupIds.has(grant.targetId);
|
||||
if (grant.targetType === "user") return grant.targetId === user.id;
|
||||
return false;
|
||||
}),
|
||||
exceptions: data.exceptions.filter((exception) => exception.userId === user.id),
|
||||
syncStatuses: [],
|
||||
auditEvents: [],
|
||||
taskManagerMemberships: data.taskManagerMemberships.filter(
|
||||
(membership) => membership.userId === user.id && clientIds.has(membership.clientId)
|
||||
),
|
||||
taskManagerProjectMemberships: data.taskManagerProjectMemberships.filter(
|
||||
(membership) => membership.userId === user.id && clientIds.has(membership.clientId)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function cookieOptions(maxAgeMs) {
|
||||
const options = {
|
||||
httpOnly: true,
|
||||
|
||||
Reference in New Issue
Block a user