All files / src/lib flowsheetAccess.ts

85.18% Statements 23/27
87.5% Branches 14/16
100% Functions 2/2
85.18% Lines 23/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                            4845x         72x     79x         4x 4x           4x                   330x     7914x           7914x 7914x         7392x             3528x     2224x       1260x     44x           522x     76x                               8244x             7100x       1143x     1x       1x 1x      
import type { FlowsheetAccess, FlowsheetRead } from "@/api/apiStore.gen";
 
export type FlowsheetWithAccess = Omit<FlowsheetRead, "access"> & {
  access?: FlowsheetAccess;
};
 
export type SharedUserAccess = {
  email: string;
  read_only: boolean;
};
 
export function getFlowsheetAccess(
  project?: FlowsheetRead,
): FlowsheetAccess | undefined {
  return (project as FlowsheetWithAccess | undefined)?.access;
}
 
export function normalizeSharedUsers(users: unknown): SharedUserAccess[] {
  if (!Array.isArray(users)) {
    return [];
  }
 
  return users.flatMap((user) => {
    if (!user || typeof user !== "object") {
      return [];
    }
 
    const email = "email" in user ? user.email : undefined;
    const readOnly = "read_only" in user ? user.read_only : undefined;
 
    if (typeof email !== "string") {
      return [];
    }
 
    return [{ email, read_only: Boolean(readOnly) }];
  });
}
 
export function getCachedFlowsheetAccess(
  state: Record<string, any> | undefined,
  reducerPath: string,
  flowsheetId: number | null,
): FlowsheetAccess | undefined {
  if (!state || !reducerPath || !flowsheetId) {
    return undefined;
  }
 
  const queries = state[reducerPath]?.queries;
 
  if (!queries || typeof queries !== "object") {
    return undefined;
  }
 
  const directQueryKey = `coreFlowsheetsRetrieve({"id":"${flowsheetId}"})`;
  const directData = queries[directQueryKey]?.data as
    | FlowsheetWithAccess
    | undefined;
 
  if (directData?.access) {
    return directData.access;
  }
 
  // Some screens only have the flowsheet cached via list-style queries, so fall
  // back to scanning the RTK Query cache instead of requiring a dedicated detail
  // fetch before the UI can decide whether to disable mutation controls.
  for (const value of Object.values(queries)) {
    const data = (value as { data?: unknown } | undefined)?.data;
 
    if (!data || typeof data !== "object") {
      continue;
    }
 
    if ((data as { id?: unknown }).id !== flowsheetId) {
      continue;
    }
 
    const access = (data as FlowsheetWithAccess).access;
    if (access) {
      return access;
    }
  }
 
  return undefined;
}
 
const READ_ONLY_ALLOWED_MUTATION_PATH_SEGMENTS = [
  "/api/flowsheet/copy",
  "/download",
  "/download_",
  "/export",
];
 
export function isReadOnlyMutationBlocked({
  method,
  pathname,
  access,
}: {
  method: string;
  pathname: string;
  access?: FlowsheetAccess;
}) {
  const normalizedMethod = method.toUpperCase();
 
  if (
    normalizedMethod === "GET" ||
    normalizedMethod === "HEAD" ||
    normalizedMethod === "OPTIONS"
  ) {
    return false;
  }
 
  if (!access?.read_only) {
    return false;
  }
 
  const normalizedPath = pathname.toLowerCase();
 
  // Copy / download / export are intentionally still allowed for read-only
  // shares, because they do not mutate the shared source flowsheet itself.
  return !READ_ONLY_ALLOWED_MUTATION_PATH_SEGMENTS.some((segment) =>
    normalizedPath.includes(segment),
  );
}