0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

Merge pull request #5529 from logto-io/yemq-log-8446-update-user-context-type

refactor(core): update user context type
This commit is contained in:
Darcy Ye 2024-03-21 10:54:46 +08:00 committed by GitHub
commit f727ef8415
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 109 additions and 112 deletions

View file

@ -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. */

View file

@ -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;

View file

@ -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;
}

View file

@ -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)

View file

@ -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;
}

View file

@ -1,77 +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) => {
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 },
} = queries;
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);
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 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 })
)),
};
})
),
};
})
),
// 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')),
};
})
),
},
])
)
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,
}))
),
};

View file

@ -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,
};
};

View file

@ -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 }),

View file

@ -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);

View file

@ -1,43 +1,33 @@
import { z } from 'zod';
import {
OrganizationRoles,
OrganizationScopes,
Resources,
Roles,
Scopes,
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';
import { scopeResponseGuard } from './scope.js';
import { userInfoGuard } from './user.js';
const organizationDetailGuard = z.object({
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.record(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(),
})
.array(),
});
export type JwtCustomizerUserContext = z.infer<typeof jwtCustomizerUserContextGuard>;

View file

@ -19,7 +19,11 @@ export const userInfoSelectFields = Object.freeze([
] as const);
export const userInfoGuard = Users.guard.pick(
Object.fromEntries(userInfoSelectFields.map((key) => [key, true]))
// 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>;