All files / src/lib flowsheetAccess.ts

82.14% Statements 23/28
87.5% Branches 14/16
100% Functions 2/2
82.14% Lines 23/28

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                                                107918x         231x     7904x                           25x 25x           25x                   270x     5451x           5451x 5451x         5034x             2758x     1718x       997x     43x           417x     62x                               5721x             4487x       1233x     1x       1x 1x      
import type { FlowsheetRead } from "@/api/apiStore.gen";
 
export type FlowsheetAccess = {
  is_owner: boolean;
  read_only: boolean;
  can_edit: boolean;
  can_share: boolean;
  can_copy: boolean;
  can_export: boolean;
  can_manage_template_settings: boolean;
};
 
export type FlowsheetWithAccess = FlowsheetRead & {
  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 (typeof user === "string") {
      // Backward compatibility for older API responses from before share-level
      // permissions existed: the frontend used to receive `string[]` emails only.
      // Treat those temporary legacy entries as editable so the UI preserves the
      // historical shared-user behaviour until the generated schema/client is
      // regenerated everywhere to the structured `{ email, read_only }` shape.
      return [{ email: user, read_only: false }];
    }
 
    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),
  );
}