All files / src/lib flowsheetAccess.ts

90.9% Statements 30/33
85% Branches 17/20
100% Functions 3/3
90.9% Lines 30/33

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 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159                            216642x                 215082x       1200x             135x                         100x     106x         7x 7x           7x                   541x     12452x   12452x               12452x 12452x     12452x 52306x       11382x             10183x     5724x       4336x     123x   47x       1023x     103x                               12993x             11654x       1337x     2x       2x 2x      
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;
}
 
/** Derive immutable preview capabilities without changing cached access data. */
export function deriveRevisionPreviewAccess(
  access: FlowsheetAccess | undefined,
  revisionSelected: boolean,
): FlowsheetAccess | undefined {
  if (!revisionSelected) {
    return access;
  }
 
  if (access) {
    return {
      ...access,
      read_only: true,
      can_edit: false,
    };
  }
 
  return {
    is_owner: false,
    read_only: true,
    can_edit: false,
    can_share: false,
    can_copy: false,
    can_export: false,
    can_manage_template_settings: false,
  };
}
 
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, unknown> | undefined,
  reducerPath: string,
  flowsheetId: number | null,
): FlowsheetAccess | undefined {
  if (!state || !reducerPath || !flowsheetId) {
    return undefined;
  }
 
  const reducerState = state[reducerPath];
  const queries =
    reducerState && typeof reducerState === "object"
      ? (reducerState as { queries?: unknown }).queries
      : undefined;
 
  if (!queries || typeof queries !== "object") {
    return undefined;
  }
 
  const directQueryPrefix = `coreFlowsheetsRetrieve({"id":"${flowsheetId}"})`;
  const queryEntries = Object.entries(
    queries as Record<string, { data?: unknown }>,
  );
  const directData = queryEntries.find(([queryKey]) =>
    queryKey.startsWith(directQueryPrefix),
  )?.[1]?.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 queryEntries) {
    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),
  );
}