0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

Merge pull request #5487 from logto-io/yemq-log-8357-prepare-jwt-user-info-context

feat(core,schemas): add JWT customizer user info context
This commit is contained in:
Darcy Ye 2024-03-12 11:01:31 +08:00 committed by GitHub
commit c8eaa45417
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 130 additions and 5 deletions

View file

@ -0,0 +1,84 @@
import type { JwtCustomizerUserContext } from '@logto/schemas';
import {
userInfoSelectFields,
OrganizationScopes,
jwtCustomizerUserContextGuard,
} from '@logto/schemas';
import { deduplicate, pick, pickState } from '@silverhand/essentials';
import { type UserLibrary } from '#src/libraries/user.js';
import type Queries from '#src/tenants/Queries.js';
export const createJwtCustomizerLibrary = (queries: Queries, userLibrary: UserLibrary) => {
const {
users: { findUserById },
rolesScopes: { findRolesScopesByRoleId },
scopes: { findScopeById },
resources: { findResourceById },
userSsoIdentities,
organizations: { relations },
} = queries;
const { findUserRoles } = userLibrary;
const getUserContext = async (userId: string): Promise<JwtCustomizerUserContext> => {
const user = await findUserById(userId);
const fullSsoIdentities = await userSsoIdentities.findUserSsoIdentitiesByUserId(userId);
const roles = await findUserRoles(userId);
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')),
};
})
),
},
])
)
),
};
return jwtCustomizerUserContextGuard.parse(userContext);
};
return {
getUserContext,
};
};

View file

@ -22,11 +22,7 @@ export const encryptUserPassword = async (
passwordEncryptionMethod: UsersPasswordEncryptionMethod;
}> => {
const passwordEncryptionMethod = UsersPasswordEncryptionMethod.Argon2i;
const passwordEncrypted = await encryptPassword(
password,
passwordEncryptionMethod
);
const passwordEncrypted = await encryptPassword(password, passwordEncryptionMethod);
return { passwordEncrypted, passwordEncryptionMethod };
};

View file

@ -3,6 +3,7 @@ import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js'
import type { ConnectorLibrary } from '#src/libraries/connector.js';
import { createDomainLibrary } from '#src/libraries/domain.js';
import { createHookLibrary } from '#src/libraries/hook/index.js';
import { createJwtCustomizerLibrary } from '#src/libraries/jwt-customizer.js';
import { OrganizationInvitationLibrary } from '#src/libraries/organization-invitation.js';
import { createPasscodeLibrary } from '#src/libraries/passcode.js';
import { createPhraseLibrary } from '#src/libraries/phrase.js';
@ -22,6 +23,7 @@ export default class Libraries {
phrases = createPhraseLibrary(this.queries);
hooks = createHookLibrary(this.queries);
socials = createSocialLibrary(this.queries, this.connectors);
jwtCustomizers = createJwtCustomizerLibrary(this.queries, this.users);
passcodes = createPasscodeLibrary(this.queries, this.connectors);
applications = createApplicationLibrary(this.queries);
verificationStatuses = createVerificationStatusLibrary(this.queries);

View file

@ -26,3 +26,4 @@ export * from './tenant.js';
export * from './tenant-organization.js';
export * from './mapi-proxy.js';
export * from './consent.js';
export * from './jwt-customizer.js';

View file

@ -0,0 +1,42 @@
import { z } from 'zod';
import {
OrganizationRoles,
OrganizationScopes,
Resources,
Roles,
Scopes,
UserSsoIdentities,
} from '../db-entries/index.js';
import { mfaFactorsGuard } from '../foundations/index.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 })
),
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 }))
),
})
),
organizations: z.record(organizationDetailGuard),
});
export type JwtCustomizerUserContext = z.infer<typeof jwtCustomizerUserContextGuard>;