Browse Source

add scope-utils for perse scopes in cliant

NaokiHigashi28 1 year ago
parent
commit
92272f870f

+ 7 - 137
apps/app/src/client/components/Me/AccessTokenScopeSelect.tsx

@@ -3,26 +3,10 @@ import React from 'react';
 import type { UseFormRegisterReturn } from 'react-hook-form';
 import type { UseFormRegisterReturn } from 'react-hook-form';
 import { v4 as uuid } from 'uuid';
 import { v4 as uuid } from 'uuid';
 
 
+import { parseScopes } from '~/client/util/scope-util';
+
 import { SCOPE } from '../../../interfaces/scope';
 import { SCOPE } from '../../../interfaces/scope';
 
 
-/** Top-level READ/WRITE structure for scope definitions */
-interface ScopePermissions {
-  READ?: PermissionBranch;
-  WRITE?: PermissionBranch;
-  [key: string]: unknown;
-}
-
-/** Nested permission object, e.g. { ADMIN: { ALL: "read:admin:*", TOP: { ... } } } */
-interface PermissionBranch {
-  [key: string]: PermissionBranch | string;
-}
-
-/** The merged node includes optional read/write strings plus any nested keys */
-interface MergedNode {
-  read?: string;
-  write?: string;
-  [key: string]: MergedNode | string | undefined;
-}
 
 
 /**
 /**
  * After we transform the merged tree, we end up with a structure
  * After we transform the merged tree, we end up with a structure
@@ -45,9 +29,11 @@ type AccessTokenScopeSelectProps = {
  * Displays a list of permissions in a recursive, nested checkbox interface.
  * Displays a list of permissions in a recursive, nested checkbox interface.
  */
  */
 export const AccessTokenScopeSelect: React.FC<AccessTokenScopeSelectProps> = ({ register }) => {
 export const AccessTokenScopeSelect: React.FC<AccessTokenScopeSelectProps> = ({ register }) => {
+  console.log(JSON.stringify(SCOPE, null, 2));
+  console.log(JSON.stringify(parseScopes(SCOPE), null, 2));
   return (
   return (
     <div className="border rounded">
     <div className="border rounded">
-      <RecursiveScopeList scopeObject={parsePermissions(SCOPE)} register={register} />
+      <RecursiveScopeList scopeObject={parseScopes(SCOPE)} register={register} />
     </div>
     </div>
   );
   );
 };
 };
@@ -139,129 +125,13 @@ const RecursiveScopeList: React.FC<RecursiveScopeListProps> = ({
                 {...register}
                 {...register}
               />
               />
               <label className="form-check-label ms-2" htmlFor={scopeValue as string}>
               <label className="form-check-label ms-2" htmlFor={scopeValue as string}>
-                {scopeValue as string}
+                {scopeKey as string}
               </label>
               </label>
             </div>
             </div>
-            <div className="col fs-6 text-secondary">desc for {scopeValue as string}</div>
+            <div className="col fs-6 text-secondary">desc for {scopeKey as string}</div>
           </div>
           </div>
         );
         );
       })}
       })}
     </>
     </>
   );
   );
 };
 };
-
-/**
- * Build an intermediate tree structure merging READ/WRITE branches.
- * "ALL" keys in nested levels merge into the parent node’s read/write.
- */
-function buildMergedTree(permissions: ScopePermissions): MergedNode {
-  const root: MergedNode = {};
-
-  // Recursively traverse each subtree under a READ or WRITE key
-  function traverse(
-      obj: PermissionBranch,
-      action: 'read' | 'write',
-      path: string[],
-  ) {
-    for (const [key, value] of Object.entries(obj)) {
-      const lowerKey = key.toLowerCase();
-
-      // Leaf node is a string like "read:admin:*"
-      if (typeof value === 'string') {
-        // If the key is "ALL" and we're not at the top level, merge directly into the parent
-        if (lowerKey === 'all' && path.length > 0) {
-          const parentNode = getOrCreateNode(root, path);
-          parentNode[action] = value;
-        }
-        else {
-          // Otherwise, create/obtain the subnode and set read/write
-          const node = getOrCreateNode(root, [...path, lowerKey]);
-          node[action] = value;
-        }
-      }
-
-      // If it’s another object, recurse deeper
-      else if (value && typeof value === 'object') {
-        if (lowerKey === 'all' && path.length > 0) {
-          // If deeper levels under "ALL", merge them into the same path
-          traverse(value as PermissionBranch, action, path);
-        }
-        else {
-          traverse(value as PermissionBranch, action, [...path, lowerKey]);
-        }
-      }
-    }
-  }
-
-  // Helper to walk the path array and create/fetch a node in the root
-  function getOrCreateNode(base: MergedNode, segments: string[]): MergedNode {
-    let curr = base;
-    for (const seg of segments) {
-      if (!curr[seg]) {
-        curr[seg] = {};
-      }
-      curr = curr[seg] as MergedNode;
-    }
-    return curr;
-  }
-
-  // Process top-level READ/WRITE objects
-  for (const [actionKey, subtree] of Object.entries(permissions)) {
-    const action = actionKey.toLowerCase() === 'read' ? 'read' : 'write';
-    if (subtree && typeof subtree === 'object') {
-      traverse(subtree as PermissionBranch, action, []);
-    }
-  }
-
-  return root;
-}
-
-/**
- * Convert the merged tree to final structure:
- * - Insert "read:xyz" / "write:xyz" as keys for each node
- * - Uppercase sub-node keys for further nesting
- */
-function transformTree(node: MergedNode, path: string): TransformedNode {
-  const result: TransformedNode = {};
-
-  // If the node has read/write, assign them as new keys
-  if (node.read) {
-    result[`read:${path}`] = node.read;
-  }
-  if (node.write) {
-    result[`write:${path}`] = node.write;
-  }
-
-  // For each nested key, transform recursively
-  for (const [k, v] of Object.entries(node)) {
-    if (k === 'read' || k === 'write') continue;
-
-    const subPath = path ? `${path}:${k}` : k;
-    const upperKey = path ? `${path}:${k}`.toUpperCase() : k.toUpperCase();
-
-    if (typeof v === 'object' && v !== null) {
-      result[upperKey] = transformTree(v as MergedNode, subPath);
-    }
-  }
-  return result;
-}
-
-/**
- * Main function that:
- * 1) Merges the SCOPE’s READ/WRITE objects into one intermediate tree.
- * 2) Transforms that intermediate tree into an object of type { [key: string]: string | object }.
- */
-function parsePermissions(permissions: ScopePermissions): Record<string, TransformedNode> {
-  const merged = buildMergedTree(permissions);
-  const result: Record<string, TransformedNode> = {};
-
-  // Transform each top-level key of the merged tree
-  for (const [topKey, node] of Object.entries(merged)) {
-    const upperKey = topKey.toUpperCase();
-    if (typeof node === 'object' && node !== null) {
-      result[upperKey] = transformTree(node as MergedNode, topKey);
-    }
-  }
-
-  return result;
-}

+ 88 - 0
apps/app/src/client/util/scope-util.ts

@@ -0,0 +1,88 @@
+
+// Data structure for the final merged scopes
+interface ScopeMap {
+  [key: string]: string | ScopeMap;
+}
+
+// Input object with arbitrary action keys (e.g., READ, WRITE)
+type ScopesInput = Record<string, any>;
+
+
+function parseSubScope(
+    parentKey: string,
+    subObjForActions: Record<string, any>,
+    actions: string[],
+): ScopeMap {
+  const result: ScopeMap = {};
+
+  for (const action of actions) {
+    if (typeof subObjForActions[action] === 'string') {
+      result[`${action.toLowerCase()}:${parentKey.toLowerCase()}:all`] = subObjForActions[action];
+      subObjForActions[action] = undefined;
+    }
+  }
+
+  const childKeys = new Set<string>();
+  for (const action of actions) {
+    const obj = subObjForActions[action];
+    if (obj && typeof obj === 'object') {
+      Object.keys(obj).forEach(k => childKeys.add(k));
+    }
+  }
+
+  for (const ck of childKeys) {
+    if (ck === 'ALL') {
+      for (const action of actions) {
+        const val = subObjForActions[action]?.[ck];
+        if (typeof val === 'string') {
+          result[`${action.toLowerCase()}:${parentKey.toLowerCase()}:all`] = val;
+        }
+      }
+      continue;
+    }
+
+    const newKey = `${parentKey}:${ck}`;
+    const childSubObj: Record<string, any> = {};
+    for (const action of actions) {
+      childSubObj[action] = subObjForActions[action]?.[ck];
+    }
+
+    result[newKey] = parseSubScope(newKey, childSubObj, actions);
+  }
+
+  return result;
+}
+
+export function parseScopes(scopes: ScopesInput): ScopeMap {
+  const actions = Object.keys(scopes);
+  const topKeys = new Set<string>();
+
+  // Collect all top-level keys (e.g., ALL, ADMIN, USER) across all actions
+  for (const action of actions) {
+    Object.keys(scopes[action] || {}).forEach(k => topKeys.add(k));
+  }
+
+  const result: ScopeMap = {};
+
+  for (const key of topKeys) {
+    if (key === 'ALL') {
+      const allObj: ScopeMap = {};
+      for (const action of actions) {
+        const val = scopes[action]?.[key];
+        if (typeof val === 'string') {
+          allObj[`${action.toLowerCase()}:all`] = val;
+        }
+      }
+      result.ALL = allObj;
+    }
+    else {
+      const subObjForActions: Record<string, any> = {};
+      for (const action of actions) {
+        subObjForActions[action] = scopes[action]?.[key];
+      }
+      result[key] = parseSubScope(key, subObjForActions, actions);
+    }
+  }
+
+  return result;
+}