From 348124b60e67ea017ba60e83a645310ffd07ce82 Mon Sep 17 00:00:00 2001
From: Darcy Ye <darcyye@silverhand.io>
Date: Tue, 19 Mar 2024 16:01:55 +0800
Subject: [PATCH 1/5] refactor(core): update user context type

---
 packages/core/src/libraries/jwt-customizer.ts | 40 ++++++++-----------
 packages/schemas/src/types/jwt-customizer.ts  |  3 +-
 packages/schemas/src/types/user.ts            | 21 ++++++++--
 3 files changed, 37 insertions(+), 27 deletions(-)

diff --git a/packages/core/src/libraries/jwt-customizer.ts b/packages/core/src/libraries/jwt-customizer.ts
index 2fba78875..e51270a07 100644
--- a/packages/core/src/libraries/jwt-customizer.ts
+++ b/packages/core/src/libraries/jwt-customizer.ts
@@ -49,29 +49,23 @@ export const createJwtCustomizerLibrary = (queries: Queries, userLibrary: UserLi
           };
         })
       ),
-      // No need to deal with the type here, the type will be enforced by the guard when return the result.
-      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-      organizations: Object.fromEntries(
-        await Promise.all(
-          organizationsWithRoles.map(async ({ organizationRoles, ...organization }) => [
-            organization.id,
-            {
-              roles: await Promise.all(
-                organizationRoles.map(async ({ id, name }) => {
-                  const [_, fullOrganizationScopes] = await relations.rolesScopes.getEntities(
-                    OrganizationScopes,
-                    { organizationRoleId: id }
-                  );
-                  return {
-                    id,
-                    name,
-                    scopes: fullOrganizationScopes.map(pickState('id', 'name', 'description')),
-                  };
-                })
-              ),
-            },
-          ])
-        )
+      organizations: await Promise.all(
+        organizationsWithRoles.map(async ({ organizationRoles, ...organization }) => ({
+          id: organization.id,
+          roles: await Promise.all(
+            organizationRoles.map(async ({ id, name }) => {
+              const [_, fullOrganizationScopes] = await relations.rolesScopes.getEntities(
+                OrganizationScopes,
+                { organizationRoleId: id }
+              );
+              return {
+                id,
+                name,
+                scopes: fullOrganizationScopes.map(pickState('id', 'name', 'description')),
+              };
+            })
+          ),
+        }))
       ),
     };
 
diff --git a/packages/schemas/src/types/jwt-customizer.ts b/packages/schemas/src/types/jwt-customizer.ts
index ca4b5c551..6d6e0b733 100644
--- a/packages/schemas/src/types/jwt-customizer.ts
+++ b/packages/schemas/src/types/jwt-customizer.ts
@@ -14,6 +14,7 @@ import { jwtCustomizerGuard } from './logto-config/index.js';
 import { userInfoGuard } from './user.js';
 
 const organizationDetailGuard = z.object({
+  id: z.string(),
   roles: z.array(
     OrganizationRoles.guard.pick({ id: true, name: true }).extend({
       scopes: z.array(OrganizationScopes.guard.pick({ id: true, name: true, description: true })),
@@ -37,7 +38,7 @@ export const jwtCustomizerUserContextGuard = userInfoGuard.extend({
       ),
     })
   ),
-  organizations: z.record(organizationDetailGuard),
+  organizations: z.array(organizationDetailGuard),
 });
 
 export type JwtCustomizerUserContext = z.infer<typeof jwtCustomizerUserContextGuard>;
diff --git a/packages/schemas/src/types/user.ts b/packages/schemas/src/types/user.ts
index d51d16203..ea059930f 100644
--- a/packages/schemas/src/types/user.ts
+++ b/packages/schemas/src/types/user.ts
@@ -18,9 +18,24 @@ export const userInfoSelectFields = Object.freeze([
   'isSuspended',
 ] as const);
 
-export const userInfoGuard = Users.guard.pick(
-  Object.fromEntries(userInfoSelectFields.map((key) => [key, true]))
-);
+/**
+ * The `pick` method of previous implementation will be overridden by `merge`/`extend` method, should explicitly specify keys in `pick` method.
+ * DO REMEMBER TO UPDATE THIS GUARD WHEN YOU UPDATE `userInfoSelectFields`.
+ */
+export const userInfoGuard = Users.guard.pick({
+  id: true,
+  username: true,
+  primaryEmail: true,
+  primaryPhone: true,
+  name: true,
+  avatar: true,
+  customData: true,
+  identities: true,
+  lastSignInAt: true,
+  createdAt: true,
+  applicationId: true,
+  isSuspended: true,
+});
 
 export type UserInfo = z.infer<typeof userInfoGuard>;
 

From 8f5baac585efdc9d5df4d7782b86cd72a13e5848 Mon Sep 17 00:00:00 2001
From: Darcy Ye <darcyye@silverhand.io>
Date: Wed, 20 Mar 2024 00:47:56 +0800
Subject: [PATCH 2/5] refactor(core,schemas): refactor to improve lib method
 performance

---
 packages/core/src/libraries/jwt-customizer.ts | 87 +++++++++----------
 packages/core/src/libraries/scope.ts          | 30 +++++++
 packages/core/src/routes/role.scope.ts        | 18 +---
 packages/core/src/tenants/Libraries.ts        |  4 +-
 packages/schemas/src/types/jwt-customizer.ts  | 47 +++++-----
 packages/schemas/src/types/user.ts            | 25 ++----
 6 files changed, 102 insertions(+), 109 deletions(-)
 create mode 100644 packages/core/src/libraries/scope.ts

diff --git a/packages/core/src/libraries/jwt-customizer.ts b/packages/core/src/libraries/jwt-customizer.ts
index e51270a07..36f437145 100644
--- a/packages/core/src/libraries/jwt-customizer.ts
+++ b/packages/core/src/libraries/jwt-customizer.ts
@@ -1,71 +1,64 @@
 import type { JwtCustomizerUserContext } from '@logto/schemas';
-import {
-  userInfoSelectFields,
-  OrganizationScopes,
-  jwtCustomizerUserContextGuard,
-} from '@logto/schemas';
+import { userInfoSelectFields, jwtCustomizerUserContextGuard } from '@logto/schemas';
 import { deduplicate, pick, pickState } from '@silverhand/essentials';
 
+import { type ScopeLibrary } from '#src/libraries/scope.js';
 import { type UserLibrary } from '#src/libraries/user.js';
 import type Queries from '#src/tenants/Queries.js';
 
-export const createJwtCustomizerLibrary = (queries: Queries, userLibrary: UserLibrary) => {
+// Show top 20 organization roles.
+const limit = 20;
+const offset = 0;
+
+export const createJwtCustomizerLibrary = (
+  queries: Queries,
+  userLibrary: UserLibrary,
+  scopeLibrary: ScopeLibrary
+) => {
   const {
     users: { findUserById },
-    rolesScopes: { findRolesScopesByRoleId },
-    scopes: { findScopeById },
-    resources: { findResourceById },
+    rolesScopes: { findRolesScopesByRoleIds },
+    scopes: { findScopesByIds },
     userSsoIdentities,
-    organizations: { relations },
+    organizations: { relations, roles: organizationRoles },
   } = queries;
   const { findUserRoles } = userLibrary;
+  const { attachResourceToScopes } = scopeLibrary;
 
   const getUserContext = async (userId: string): Promise<JwtCustomizerUserContext> => {
     const user = await findUserById(userId);
     const fullSsoIdentities = await userSsoIdentities.findUserSsoIdentitiesByUserId(userId);
     const roles = await findUserRoles(userId);
+    const rolesScopes = await findRolesScopesByRoleIds(roles.map(({ id }) => id));
+    const scopeIds = rolesScopes.map(({ scopeId }) => scopeId);
+    const scopes = await findScopesByIds(scopeIds);
+    const scopesWithResources = await attachResourceToScopes(scopes);
     const organizationsWithRoles = await relations.users.getOrganizationsByUserId(userId);
+    const [_, organizationRolesWithScopes] = await organizationRoles.findAll(limit, offset);
     const userContext = {
       ...pick(user, ...userInfoSelectFields),
       ssoIdentities: fullSsoIdentities.map(pickState('issuer', 'identityId', 'detail')),
       mfaVerificationFactors: deduplicate(user.mfaVerifications.map(({ type }) => type)),
-      roles: await Promise.all(
-        roles.map(async (role) => {
-          const fullRolesScopes = await findRolesScopesByRoleId(role.id);
-          const scopeIds = fullRolesScopes.map(({ scopeId }) => scopeId);
-          return {
-            ...pick(role, 'id', 'name', 'description'),
-            scopes: await Promise.all(
-              scopeIds.map(async (scopeId) => {
-                const scope = await findScopeById(scopeId);
-                return {
-                  ...pick(scope, 'id', 'name', 'description'),
-                  ...(await findResourceById(scope.resourceId).then(
-                    ({ indicator, id: resourceId }) => ({ indicator, resourceId })
-                  )),
-                };
-              })
-            ),
-          };
-        })
-      ),
-      organizations: await Promise.all(
-        organizationsWithRoles.map(async ({ organizationRoles, ...organization }) => ({
-          id: organization.id,
-          roles: await Promise.all(
-            organizationRoles.map(async ({ id, name }) => {
-              const [_, fullOrganizationScopes] = await relations.rolesScopes.getEntities(
-                OrganizationScopes,
-                { organizationRoleId: id }
-              );
-              return {
-                id,
-                name,
-                scopes: fullOrganizationScopes.map(pickState('id', 'name', 'description')),
-              };
-            })
-          ),
-        }))
+      roles: roles.map((role) => {
+        const scopeIds = new Set(
+          rolesScopes.filter(({ roleId }) => roleId === role.id).map(({ scopeId }) => scopeId)
+        );
+        return {
+          ...pick(role, 'id', 'name', 'description'),
+          scopes: scopesWithResources
+            .filter(({ id }) => scopeIds.has(id))
+            .map(pickState('id', 'name', 'description', 'resourceId', 'resource')),
+        };
+      }),
+      organizations: organizationsWithRoles.map(pickState('id', 'name', 'description')),
+      organizationRoles: organizationsWithRoles.flatMap(
+        ({ id: organizationId, organizationRoles }) =>
+          organizationRoles.map(({ id: roleId, name: roleName }) => ({
+            organizationId,
+            roleId,
+            roleName,
+            scopes: organizationRolesWithScopes.find(({ id }) => id === roleId)?.scopes ?? [],
+          }))
       ),
     };
 
diff --git a/packages/core/src/libraries/scope.ts b/packages/core/src/libraries/scope.ts
new file mode 100644
index 000000000..2b6d60fed
--- /dev/null
+++ b/packages/core/src/libraries/scope.ts
@@ -0,0 +1,30 @@
+import type { Scope, ScopeResponse } from '@logto/schemas';
+
+import type Queries from '#src/tenants/Queries.js';
+import assertThat from '#src/utils/assert-that.js';
+
+export type ScopeLibrary = ReturnType<typeof createScopeLibrary>;
+
+export const createScopeLibrary = (queries: Queries) => {
+  const {
+    resources: { findResourcesByIds },
+  } = queries;
+
+  const attachResourceToScopes = async (scopes: readonly Scope[]): Promise<ScopeResponse[]> => {
+    const resources = await findResourcesByIds(scopes.map(({ resourceId }) => resourceId));
+    return scopes.map((scope) => {
+      const resource = resources.find(({ id }) => id === scope.resourceId);
+
+      assertThat(resource, new Error(`Cannot find resource for id ${scope.resourceId}`));
+
+      return {
+        ...scope,
+        resource,
+      };
+    });
+  };
+
+  return {
+    attachResourceToScopes,
+  };
+};
diff --git a/packages/core/src/routes/role.scope.ts b/packages/core/src/routes/role.scope.ts
index 8cbafdf39..61b731cb6 100644
--- a/packages/core/src/routes/role.scope.ts
+++ b/packages/core/src/routes/role.scope.ts
@@ -1,4 +1,3 @@
-import type { Scope, ScopeResponse } from '@logto/schemas';
 import { scopeResponseGuard, Scopes } from '@logto/schemas';
 import { generateStandardId } from '@logto/shared';
 import { tryThat } from '@silverhand/essentials';
@@ -7,7 +6,6 @@ import { object, string } from 'zod';
 import RequestError from '#src/errors/RequestError/index.js';
 import koaGuard from '#src/middleware/koa-guard.js';
 import koaPagination from '#src/middleware/koa-pagination.js';
-import assertThat from '#src/utils/assert-that.js';
 import { parseSearchParamsForSearch } from '#src/utils/search.js';
 
 import type { AuthedRouter, RouterInitArgs } from './types.js';
@@ -16,7 +14,6 @@ export default function roleScopeRoutes<T extends AuthedRouter>(
   ...[router, { queries, libraries }]: RouterInitArgs<T>
 ) {
   const {
-    resources: { findResourcesByIds },
     rolesScopes: { deleteRolesScope, findRolesScopesByRoleId, insertRolesScopes },
     roles: { findRoleById },
     scopes: { findScopesByIds, countScopesByScopeIds, searchScopesByScopeIds },
@@ -24,22 +21,9 @@ export default function roleScopeRoutes<T extends AuthedRouter>(
   const {
     quota,
     roleScopes: { validateRoleScopeAssignment },
+    scopes: { attachResourceToScopes },
   } = libraries;
 
-  const attachResourceToScopes = async (scopes: readonly Scope[]): Promise<ScopeResponse[]> => {
-    const resources = await findResourcesByIds(scopes.map(({ resourceId }) => resourceId));
-    return scopes.map((scope) => {
-      const resource = resources.find(({ id }) => id === scope.resourceId);
-
-      assertThat(resource, new Error(`Cannot find resource for id ${scope.resourceId}`));
-
-      return {
-        ...scope,
-        resource,
-      };
-    });
-  };
-
   router.get(
     '/roles/:id/scopes',
     koaPagination({ isOptional: true }),
diff --git a/packages/core/src/tenants/Libraries.ts b/packages/core/src/tenants/Libraries.ts
index 449c92553..12a703748 100644
--- a/packages/core/src/tenants/Libraries.ts
+++ b/packages/core/src/tenants/Libraries.ts
@@ -10,6 +10,7 @@ import { createPhraseLibrary } from '#src/libraries/phrase.js';
 import { createProtectedAppLibrary } from '#src/libraries/protected-app.js';
 import { createQuotaLibrary } from '#src/libraries/quota.js';
 import { createRoleScopeLibrary } from '#src/libraries/role-scope.js';
+import { createScopeLibrary } from '#src/libraries/scope.js';
 import { createSignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js';
 import { createSocialLibrary } from '#src/libraries/social.js';
 import { createSsoConnectorLibrary } from '#src/libraries/sso-connector.js';
@@ -22,8 +23,9 @@ export default class Libraries {
   users = createUserLibrary(this.queries);
   phrases = createPhraseLibrary(this.queries);
   hooks = createHookLibrary(this.queries);
+  scopes = createScopeLibrary(this.queries);
   socials = createSocialLibrary(this.queries, this.connectors);
-  jwtCustomizers = createJwtCustomizerLibrary(this.queries, this.users);
+  jwtCustomizers = createJwtCustomizerLibrary(this.queries, this.users, this.scopes);
   passcodes = createPasscodeLibrary(this.queries, this.connectors);
   applications = createApplicationLibrary(this.queries);
   verificationStatuses = createVerificationStatusLibrary(this.queries);
diff --git a/packages/schemas/src/types/jwt-customizer.ts b/packages/schemas/src/types/jwt-customizer.ts
index 6d6e0b733..8ec51b1d3 100644
--- a/packages/schemas/src/types/jwt-customizer.ts
+++ b/packages/schemas/src/types/jwt-customizer.ts
@@ -1,44 +1,39 @@
 import { z } from 'zod';
 
 import {
-  OrganizationRoles,
+  Organizations,
   OrganizationScopes,
-  Resources,
   Roles,
-  Scopes,
   UserSsoIdentities,
 } from '../db-entries/index.js';
 import { mfaFactorsGuard, jsonObjectGuard } from '../foundations/index.js';
 
 import { jwtCustomizerGuard } from './logto-config/index.js';
+import { scopeResponseGuard } from './scope.js';
 import { userInfoGuard } from './user.js';
 
-const organizationDetailGuard = z.object({
-  id: z.string(),
-  roles: z.array(
-    OrganizationRoles.guard.pick({ id: true, name: true }).extend({
-      scopes: z.array(OrganizationScopes.guard.pick({ id: true, name: true, description: true })),
-    })
-  ),
-});
-
-export type OrganizationDetail = z.infer<typeof organizationDetailGuard>;
-
 export const jwtCustomizerUserContextGuard = userInfoGuard.extend({
-  ssoIdentities: z.array(
-    UserSsoIdentities.guard.pick({ issuer: true, identityId: true, detail: true })
-  ),
+  ssoIdentities: UserSsoIdentities.guard
+    .pick({ issuer: true, identityId: true, detail: true })
+    .array(),
   mfaVerificationFactors: mfaFactorsGuard,
-  roles: z.array(
-    Roles.guard.pick({ id: true, name: true, description: true }).extend({
-      scopes: z.array(
-        Scopes.guard
-          .pick({ id: true, name: true, description: true, resourceId: true })
-          .merge(Resources.guard.pick({ indicator: true }))
-      ),
+  roles: Roles.guard
+    .pick({ id: true, name: true, description: true })
+    .extend({
+      scopes: scopeResponseGuard
+        .pick({ id: true, name: true, description: true, resourceId: true, resource: true })
+        .array(),
     })
-  ),
-  organizations: z.array(organizationDetailGuard),
+    .array(),
+  organizations: Organizations.guard.pick({ id: true, name: true, description: true }).array(),
+  organizationRoles: z
+    .object({
+      organizationId: z.string(),
+      roleId: z.string(),
+      roleName: z.string(),
+      scopes: OrganizationScopes.guard.pick({ id: true, name: true }).array(),
+    })
+    .array(),
 });
 
 export type JwtCustomizerUserContext = z.infer<typeof jwtCustomizerUserContextGuard>;
diff --git a/packages/schemas/src/types/user.ts b/packages/schemas/src/types/user.ts
index ea059930f..a34c763ce 100644
--- a/packages/schemas/src/types/user.ts
+++ b/packages/schemas/src/types/user.ts
@@ -18,24 +18,13 @@ export const userInfoSelectFields = Object.freeze([
   'isSuspended',
 ] as const);
 
-/**
- * The `pick` method of previous implementation will be overridden by `merge`/`extend` method, should explicitly specify keys in `pick` method.
- * DO REMEMBER TO UPDATE THIS GUARD WHEN YOU UPDATE `userInfoSelectFields`.
- */
-export const userInfoGuard = Users.guard.pick({
-  id: true,
-  username: true,
-  primaryEmail: true,
-  primaryPhone: true,
-  name: true,
-  avatar: true,
-  customData: true,
-  identities: true,
-  lastSignInAt: true,
-  createdAt: true,
-  applicationId: true,
-  isSuspended: true,
-});
+export const userInfoGuard = Users.guard.pick(
+  // eslint-disable-next-line no-restricted-syntax
+  Object.fromEntries(userInfoSelectFields.map((field) => [field, true])) as Record<
+    (typeof userInfoSelectFields)[number],
+    true
+  >
+);
 
 export type UserInfo = z.infer<typeof userInfoGuard>;
 

From 8ac95a1bc20802c0f339ba87db3d6160790ffd32 Mon Sep 17 00:00:00 2001
From: Darcy Ye <darcyye@silverhand.io>
Date: Wed, 20 Mar 2024 01:12:54 +0800
Subject: [PATCH 3/5] fix(console): fix some user related type in console

---
 packages/console/src/components/ItemPreview/UserPreview.tsx | 4 ++--
 .../console/src/components/UserAccountInformation/index.tsx | 4 ++--
 .../src/pages/Roles/components/AssignToRoleModal/index.tsx  | 4 ++--
 packages/console/src/pages/UserDetails/utils.ts             | 4 ++--
 packages/console/src/utils/user.ts                          | 6 +++---
 5 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/packages/console/src/components/ItemPreview/UserPreview.tsx b/packages/console/src/components/ItemPreview/UserPreview.tsx
index 7f16a3d49..f2aaf3785 100644
--- a/packages/console/src/components/ItemPreview/UserPreview.tsx
+++ b/packages/console/src/components/ItemPreview/UserPreview.tsx
@@ -1,4 +1,4 @@
-import { type User } from '@logto/schemas';
+import { type UserInfo } from '@logto/schemas';
 import { conditional } from '@silverhand/essentials';
 
 import SuspendedTag from '@/pages/Users/components/SuspendedTag';
@@ -9,7 +9,7 @@ import UserAvatar from '../UserAvatar';
 import ItemPreview from '.';
 
 type Props = {
-  user: User;
+  user: UserInfo;
 };
 
 /** A component that renders a preview of a user. It's useful for displaying a user in a list. */
diff --git a/packages/console/src/components/UserAccountInformation/index.tsx b/packages/console/src/components/UserAccountInformation/index.tsx
index ad4fc6676..e1260b998 100644
--- a/packages/console/src/components/UserAccountInformation/index.tsx
+++ b/packages/console/src/components/UserAccountInformation/index.tsx
@@ -1,5 +1,5 @@
 import type { AdminConsoleKey } from '@logto/phrases';
-import type { User } from '@logto/schemas';
+import type { UserProfileResponse } from '@logto/schemas';
 import { conditionalArray } from '@silverhand/essentials';
 import { useState } from 'react';
 import { toast } from 'react-hot-toast';
@@ -15,7 +15,7 @@ import * as modalStyles from '@/scss/modal.module.scss';
 import * as styles from './index.module.scss';
 
 type Props = {
-  user: User;
+  user: UserProfileResponse;
   password: string;
   title: AdminConsoleKey;
   onClose: () => void;
diff --git a/packages/console/src/pages/Roles/components/AssignToRoleModal/index.tsx b/packages/console/src/pages/Roles/components/AssignToRoleModal/index.tsx
index e7b93fb97..9d6197c06 100644
--- a/packages/console/src/pages/Roles/components/AssignToRoleModal/index.tsx
+++ b/packages/console/src/pages/Roles/components/AssignToRoleModal/index.tsx
@@ -1,4 +1,4 @@
-import type { RoleResponse, User, Application } from '@logto/schemas';
+import type { RoleResponse, UserProfileResponse, Application } from '@logto/schemas';
 import { RoleType } from '@logto/schemas';
 import { useState } from 'react';
 import { toast } from 'react-hot-toast';
@@ -15,7 +15,7 @@ import { getUserTitle } from '@/utils/user';
 
 type Props =
   | {
-      entity: User;
+      entity: UserProfileResponse;
       onClose: (success?: boolean) => void;
       type: RoleType.User;
     }
diff --git a/packages/console/src/pages/UserDetails/utils.ts b/packages/console/src/pages/UserDetails/utils.ts
index 053d6f8d5..f6e32b82f 100644
--- a/packages/console/src/pages/UserDetails/utils.ts
+++ b/packages/console/src/pages/UserDetails/utils.ts
@@ -1,11 +1,11 @@
-import type { User } from '@logto/schemas';
+import type { UserProfileResponse } from '@logto/schemas';
 import { formatToInternationalPhoneNumber } from '@logto/shared/universal';
 import { conditional } from '@silverhand/essentials';
 
 import type { UserDetailsForm } from './types';
 
 export const userDetailsParser = {
-  toLocalForm: (data: User): UserDetailsForm => {
+  toLocalForm: (data: UserProfileResponse): UserDetailsForm => {
     const { primaryEmail, primaryPhone, username, name, avatar, customData } = data;
     const parsedPhoneNumber = conditional(
       primaryPhone && formatToInternationalPhoneNumber(primaryPhone)
diff --git a/packages/console/src/utils/user.ts b/packages/console/src/utils/user.ts
index 6e81cc191..03cf80907 100644
--- a/packages/console/src/utils/user.ts
+++ b/packages/console/src/utils/user.ts
@@ -1,11 +1,11 @@
-import type { User } from '@logto/schemas';
+import type { UserInfo } from '@logto/schemas';
 import { getUserDisplayName } from '@logto/shared/universal';
 import { t } from 'i18next';
 
-export const getUserTitle = (user?: User): string =>
+export const getUserTitle = (user?: UserInfo): string =>
   (user ? getUserDisplayName(user) : undefined) ?? t('admin_console.users.unnamed');
 
-export const getUserSubtitle = (user?: User) => {
+export const getUserSubtitle = (user?: UserInfo) => {
   if (!user?.name) {
     return;
   }

From e5e378d1bb9d634e2f49e96d7b4aa0ebb78f4e4f Mon Sep 17 00:00:00 2001
From: Darcy Ye <darcyye@silverhand.io>
Date: Wed, 20 Mar 2024 12:58:12 +0800
Subject: [PATCH 4/5] chore: remove org role scopes field since it relies on
 pagination setup

---
 packages/core/src/libraries/jwt-customizer.ts | 8 +-------
 packages/schemas/src/types/jwt-customizer.ts  | 8 +-------
 2 files changed, 2 insertions(+), 14 deletions(-)

diff --git a/packages/core/src/libraries/jwt-customizer.ts b/packages/core/src/libraries/jwt-customizer.ts
index 36f437145..34f13c6f1 100644
--- a/packages/core/src/libraries/jwt-customizer.ts
+++ b/packages/core/src/libraries/jwt-customizer.ts
@@ -6,10 +6,6 @@ import { type ScopeLibrary } from '#src/libraries/scope.js';
 import { type UserLibrary } from '#src/libraries/user.js';
 import type Queries from '#src/tenants/Queries.js';
 
-// Show top 20 organization roles.
-const limit = 20;
-const offset = 0;
-
 export const createJwtCustomizerLibrary = (
   queries: Queries,
   userLibrary: UserLibrary,
@@ -20,7 +16,7 @@ export const createJwtCustomizerLibrary = (
     rolesScopes: { findRolesScopesByRoleIds },
     scopes: { findScopesByIds },
     userSsoIdentities,
-    organizations: { relations, roles: organizationRoles },
+    organizations: { relations },
   } = queries;
   const { findUserRoles } = userLibrary;
   const { attachResourceToScopes } = scopeLibrary;
@@ -34,7 +30,6 @@ export const createJwtCustomizerLibrary = (
     const scopes = await findScopesByIds(scopeIds);
     const scopesWithResources = await attachResourceToScopes(scopes);
     const organizationsWithRoles = await relations.users.getOrganizationsByUserId(userId);
-    const [_, organizationRolesWithScopes] = await organizationRoles.findAll(limit, offset);
     const userContext = {
       ...pick(user, ...userInfoSelectFields),
       ssoIdentities: fullSsoIdentities.map(pickState('issuer', 'identityId', 'detail')),
@@ -57,7 +52,6 @@ export const createJwtCustomizerLibrary = (
             organizationId,
             roleId,
             roleName,
-            scopes: organizationRolesWithScopes.find(({ id }) => id === roleId)?.scopes ?? [],
           }))
       ),
     };
diff --git a/packages/schemas/src/types/jwt-customizer.ts b/packages/schemas/src/types/jwt-customizer.ts
index 8ec51b1d3..22a21a712 100644
--- a/packages/schemas/src/types/jwt-customizer.ts
+++ b/packages/schemas/src/types/jwt-customizer.ts
@@ -1,11 +1,6 @@
 import { z } from 'zod';
 
-import {
-  Organizations,
-  OrganizationScopes,
-  Roles,
-  UserSsoIdentities,
-} from '../db-entries/index.js';
+import { Organizations, Roles, UserSsoIdentities } from '../db-entries/index.js';
 import { mfaFactorsGuard, jsonObjectGuard } from '../foundations/index.js';
 
 import { jwtCustomizerGuard } from './logto-config/index.js';
@@ -31,7 +26,6 @@ export const jwtCustomizerUserContextGuard = userInfoGuard.extend({
       organizationId: z.string(),
       roleId: z.string(),
       roleName: z.string(),
-      scopes: OrganizationScopes.guard.pick({ id: true, name: true }).array(),
     })
     .array(),
 });

From a2f20df9c91ba9f000cdbd5cde05676aebbc15b0 Mon Sep 17 00:00:00 2001
From: Darcy Ye <darcyye@silverhand.io>
Date: Wed, 20 Mar 2024 14:35:08 +0800
Subject: [PATCH 5/5] chore: add comments

---
 packages/core/src/libraries/jwt-customizer.ts | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/packages/core/src/libraries/jwt-customizer.ts b/packages/core/src/libraries/jwt-customizer.ts
index 34f13c6f1..2e1d73e7b 100644
--- a/packages/core/src/libraries/jwt-customizer.ts
+++ b/packages/core/src/libraries/jwt-customizer.ts
@@ -21,6 +21,12 @@ export const createJwtCustomizerLibrary = (
   const { findUserRoles } = userLibrary;
   const { attachResourceToScopes } = scopeLibrary;
 
+  /**
+   * We does not include org roles' scopes for the following reason:
+   * 1. The org scopes query method requires `limit` and `offset` parameters. Other management API get
+   * these APIs from console setup while this library method is a backend used method.
+   * 2. Logto developers can get the org roles' id from this user context and hence query the org roles' scopes via management API.
+   */
   const getUserContext = async (userId: string): Promise<JwtCustomizerUserContext> => {
     const user = await findUserById(userId);
     const fullSsoIdentities = await userSsoIdentities.findUserSsoIdentitiesByUserId(userId);