From 84ac935c80b6c3af378fa275940963a09a4295ae Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 15 Jul 2024 19:02:57 +0800 Subject: [PATCH] feat(core,schemas): implement the register flow (#6237) * feat(core,schemas): implement the register flow implement the register flow * refactor(core,schemas): relocate the profile type defs relocate the profile type defs * fix(core): fix the validation guard logic fix the validation guard logic * fix(core): fix social and sso identity not created bug fix social and sso identity not created bug * fix(core): fix social identities profile key fix social identities profile key * fix(core): fix sso query method fix sso query method --- .../classes/experience-interaction.ts | 131 ++++++++++++------ .../src/routes/experience/classes/utils.ts | 52 ++++++- .../classes/validators/profile-validator.ts | 85 ++++++++++++ .../verifications/code-verification.ts | 21 ++- .../enterprise-sso-verification.ts | 65 ++++++++- .../verifications/password-verification.ts | 17 ++- .../verifications/social-verification.ts | 71 +++++++++- .../verifications/verification-record.ts | 20 ++- packages/core/src/routes/experience/index.ts | 4 +- packages/core/src/routes/experience/types.ts | 50 +++++++ 10 files changed, 446 insertions(+), 70 deletions(-) create mode 100644 packages/core/src/routes/experience/classes/validators/profile-validator.ts diff --git a/packages/core/src/routes/experience/classes/experience-interaction.ts b/packages/core/src/routes/experience/classes/experience-interaction.ts index 9be963602..16f80f602 100644 --- a/packages/core/src/routes/experience/classes/experience-interaction.ts +++ b/packages/core/src/routes/experience/classes/experience-interaction.ts @@ -1,5 +1,7 @@ import { type ToZodObject } from '@logto/connector-kit'; -import { InteractionEvent, VerificationType } from '@logto/schemas'; +import { InteractionEvent, type VerificationType } from '@logto/schemas'; +import { generateStandardId } from '@logto/shared'; +import { conditional } from '@silverhand/essentials'; import { z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; @@ -7,8 +9,10 @@ import { type WithLogContext } from '#src/middleware/koa-audit-log.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; -import type { Interaction } from '../types.js'; +import { interactionProfileGuard, type Interaction, type InteractionProfile } from '../types.js'; +import { getNewUserProfileFromVerificationRecord, toUserSocialIdentityData } from './utils.js'; +import { ProfileValidator } from './validators/profile-validator.js'; import { SignInExperienceValidator } from './validators/sign-in-experience-validator.js'; import { buildVerificationRecord, @@ -20,14 +24,14 @@ import { type InteractionStorage = { interactionEvent?: InteractionEvent; userId?: string; - profile?: Record; + profile?: InteractionProfile; verificationRecords?: VerificationRecordData[]; }; const interactionStorageGuard = z.object({ interactionEvent: z.nativeEnum(InteractionEvent).optional(), userId: z.string().optional(), - profile: z.object({}).optional(), + profile: interactionProfileGuard.optional(), verificationRecords: verificationRecordDataGuard.array().optional(), }) satisfies ToZodObject; @@ -39,6 +43,7 @@ const interactionStorageGuard = z.object({ */ export default class ExperienceInteraction { public readonly signInExperienceValidator: SignInExperienceValidator; + public readonly profileValidator: ProfileValidator; /** The user verification record list for the current interaction. */ private readonly verificationRecords = new Map(); @@ -65,6 +70,7 @@ export default class ExperienceInteraction { this.signInExperienceValidator = new SignInExperienceValidator(libraries, queries); if (!interactionDetails) { + this.profileValidator = new ProfileValidator(libraries, queries); return; } @@ -82,6 +88,9 @@ export default class ExperienceInteraction { this.userId = userId; this.profile = profile; + // Profile validator requires the userId for existing user profile update validation + this.profileValidator = new ProfileValidator(libraries, queries, userId); + for (const record of verificationRecords) { const instance = buildVerificationRecord(libraries, queries, record); this.verificationRecords.set(instance.type, instance); @@ -123,16 +132,16 @@ export default class ExperienceInteraction { * Identify the user using the verification record. * * - Check if the verification record exists. - * - Verify the verification record with `SignInExperienceValidator`. + * - Verify the verification record with {@link SignInExperienceValidator}. * - Create a new user using the verification record if the current interaction event is `Register`. * - Identify the user using the verification record if the current interaction event is `SignIn` or `ForgotPassword`. * - Set the user id to the current interaction. * - * @throws RequestError with 404 if the interaction event is not set - * @throws RequestError with 404 if the verification record is not found - * @throws RequestError with 400 if the verification record is not valid for the current interaction event - * @throws RequestError with 401 if the user is suspended - * @throws RequestError with 409 if the current session has already identified a different user + * @throws RequestError with 404 if the interaction event is not set. + * @throws RequestError with 404 if the verification record is not found. + * @throws RequestError with 422 if the verification record is not enabled in the SIE settings. + * @see {@link identifyExistingUser} for more exceptions that can be thrown in the SignIn and ForgotPassword events. + * @see {@link createNewUser} for more exceptions that can be thrown in the Register event. **/ public async identifyUser(verificationId: string) { const verificationRecord = this.getVerificationRecordById(verificationId); @@ -196,9 +205,20 @@ export default class ExperienceInteraction { /** Submit the current interaction result to the OIDC provider and clear the interaction data */ public async submit() { - // TODO: refine the error code assertThat(this.userId, 'session.verification_session_not_found'); + // TODO: mfa validation + // TODO: missing profile fields validation + + const { + queries: { users: userQueries }, + } = this.tenant; + + // Update user profile + await userQueries.updateUserById(this.userId, { + lastSignInAt: Date.now(), + }); + const { provider } = this.tenant; const redirectTo = await provider.interactionResult(this.ctx.req, this.ctx.res, { @@ -224,46 +244,71 @@ export default class ExperienceInteraction { return [...this.verificationRecords.values()]; } + /** + * Identify the existing user using the verification record. + * + * @throws RequestError with 400 if the verification record is not verified or not valid for identifying a user + * @throws RequestError with 404 if the user is not found + * @throws RequestError with 401 if the user is suspended + * @throws RequestError with 409 if the current session has already identified a different user + */ private async identifyExistingUser(verificationRecord: VerificationRecord) { - switch (verificationRecord.type) { - case VerificationType.Password: - case VerificationType.VerificationCode: - case VerificationType.Social: - case VerificationType.EnterpriseSso: { - // TODO: social sign-in with verified email - const { id, isSuspended } = await verificationRecord.identifyUser(); + // Check verification record can be used to identify a user using the `identifyUser` method. + // E.g. MFA verification record does not have the `identifyUser` method, cannot be used to identify a user. + assertThat( + 'identifyUser' in verificationRecord, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); - assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 })); + const { id, isSuspended } = await verificationRecord.identifyUser(); - // Throws an 409 error if the current session has already identified a different user - if (this.userId) { - assertThat( - this.userId === id, - new RequestError({ code: 'session.identity_conflict', status: 409 }) - ); - return; - } + assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 })); - this.userId = id; - break; - } - default: { - // Unsupported verification type for identification, such as MFA verification. - throw new RequestError({ code: 'session.verification_failed', status: 400 }); - } + // Throws an 409 error if the current session has already identified a different user + if (this.userId) { + assertThat( + this.userId === id, + new RequestError({ code: 'session.identity_conflict', status: 409 }) + ); + return; } + + this.userId = id; } private async createNewUser(verificationRecord: VerificationRecord) { - // TODO: To be implemented - switch (verificationRecord.type) { - case VerificationType.VerificationCode: { - break; - } - default: { - // Unsupported verification type for user creation, such as MFA verification. - throw new RequestError({ code: 'session.verification_failed', status: 400 }); - } + const { + libraries: { + users: { generateUserId, insertUser }, + }, + queries: { userSsoIdentities: userSsoIdentitiesQueries }, + } = this.tenant; + + const newProfile = await getNewUserProfileFromVerificationRecord(verificationRecord); + + await this.profileValidator.guardProfileUniquenessAcrossUsers(newProfile); + + // TODO: new user provisioning and hooks + + const { socialIdentity, enterpriseSsoIdentity, ...rest } = newProfile; + + const [user] = await insertUser( + { + id: await generateUserId(), + ...rest, + ...conditional(socialIdentity && { identities: toUserSocialIdentityData(socialIdentity) }), + }, + [] + ); + + if (enterpriseSsoIdentity) { + await userSsoIdentitiesQueries.insert({ + id: generateStandardId(), + userId: user.id, + ...enterpriseSsoIdentity, + }); } + + this.userId = user.id; } } diff --git a/packages/core/src/routes/experience/classes/utils.ts b/packages/core/src/routes/experience/classes/utils.ts index 575908b45..4ddb0534a 100644 --- a/packages/core/src/routes/experience/classes/utils.ts +++ b/packages/core/src/routes/experience/classes/utils.ts @@ -1,7 +1,17 @@ -import { SignInIdentifier, type InteractionIdentifier } from '@logto/schemas'; +import { + SignInIdentifier, + VerificationType, + type InteractionIdentifier, + type User, +} from '@logto/schemas'; +import RequestError from '#src/errors/RequestError/index.js'; import type Queries from '#src/tenants/Queries.js'; +import type { InteractionProfile } from '../types.js'; + +import { type VerificationRecord } from './verifications/index.js'; + export const findUserByIdentifier = async ( userQuery: Queries['users'], { type, value }: InteractionIdentifier @@ -18,3 +28,43 @@ export const findUserByIdentifier = async ( } } }; + +/** + * @throws {RequestError} -400 if the verification record type is not supported for user creation. + * @throws {RequestError} -400 if the verification record is not verified. + */ +export const getNewUserProfileFromVerificationRecord = async ( + verificationRecord: VerificationRecord +): Promise => { + switch (verificationRecord.type) { + case VerificationType.VerificationCode: { + return verificationRecord.toUserProfile(); + } + case VerificationType.EnterpriseSso: + case VerificationType.Social: { + const identityProfile = await verificationRecord.toUserProfile(); + const syncedProfile = await verificationRecord.toSyncedProfile(true); + return { ...identityProfile, ...syncedProfile }; + } + default: { + // Unsupported verification type for user creation, such as MFA verification. + throw new RequestError({ code: 'session.verification_failed', status: 400 }); + } + } +}; + +/** + * Convert the interaction profile `socialIdentity` to `User['identities']` data format + */ +export const toUserSocialIdentityData = ( + socialIdentity: Required['socialIdentity'] +): User['identities'] => { + const { target, userInfo } = socialIdentity; + + return { + [target]: { + userId: userInfo.id, + details: userInfo, + }, + }; +}; diff --git a/packages/core/src/routes/experience/classes/validators/profile-validator.ts b/packages/core/src/routes/experience/classes/validators/profile-validator.ts new file mode 100644 index 000000000..a2c8bcb1c --- /dev/null +++ b/packages/core/src/routes/experience/classes/validators/profile-validator.ts @@ -0,0 +1,85 @@ +import RequestError from '#src/errors/RequestError/index.js'; +import type Libraries from '#src/tenants/Libraries.js'; +import type Queries from '#src/tenants/Queries.js'; +import assertThat from '#src/utils/assert-that.js'; + +import type { InteractionProfile } from '../../types.js'; + +export class ProfileValidator { + constructor( + private readonly libraries: Libraries, + private readonly queries: Queries, + /** UserId is required for existing user profile update validation */ + private readonly userId?: string + ) {} + + public async guardProfileUniquenessAcrossUsers(profile: InteractionProfile) { + const { hasUser, hasUserWithEmail, hasUserWithPhone, hasUserWithIdentity } = this.queries.users; + const { userSsoIdentities } = this.queries; + + const { username, primaryEmail, primaryPhone, socialIdentity, enterpriseSsoIdentity } = profile; + + if (username) { + assertThat( + !(await hasUser(username)), + new RequestError({ + code: 'user.username_already_in_use', + status: 422, + }) + ); + } + + if (primaryEmail) { + assertThat( + !(await hasUserWithEmail(primaryEmail)), + new RequestError({ + code: 'user.email_already_in_use', + status: 422, + }) + ); + } + + if (primaryPhone) { + assertThat( + !(await hasUserWithPhone(primaryPhone)), + new RequestError({ + code: 'user.phone_already_in_use', + status: 422, + }) + ); + } + + if (socialIdentity) { + const { + target, + userInfo: { id }, + } = socialIdentity; + + assertThat( + !(await hasUserWithIdentity(target, id)), + new RequestError({ + code: 'user.identity_already_in_use', + status: 422, + }) + ); + } + + if (enterpriseSsoIdentity) { + const { issuer, identityId } = enterpriseSsoIdentity; + const userSsoIdentity = await userSsoIdentities.findUserSsoIdentityBySsoIdentityId( + issuer, + identityId + ); + + assertThat( + !userSsoIdentity, + new RequestError({ + code: 'user.identity_already_in_use', + status: 422, + }) + ); + } + + // TODO: Password validation + } +} diff --git a/packages/core/src/routes/experience/classes/verifications/code-verification.ts b/packages/core/src/routes/experience/classes/verifications/code-verification.ts index e87aceed9..e536cc4d2 100644 --- a/packages/core/src/routes/experience/classes/verifications/code-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/code-verification.ts @@ -18,7 +18,7 @@ import assertThat from '#src/utils/assert-that.js'; import { findUserByIdentifier } from '../utils.js'; -import { type VerificationRecord } from './verification-record.js'; +import { type IdentifierVerificationRecord } from './verification-record.js'; const eventToTemplateTypeMap: Record = { SignIn: TemplateType.SignIn, @@ -67,7 +67,9 @@ const getPasscodeIdentifierPayload = ( * * To avoid the redundant naming, the `CodeVerification` is used instead of `VerificationCodeVerification`. */ -export class CodeVerification implements VerificationRecord { +export class CodeVerification + implements IdentifierVerificationRecord +{ /** * Factory method to create a new CodeVerification record using the given identifier. */ @@ -164,10 +166,6 @@ export class CodeVerification implements VerificationRecord { assertThat( this.verified, @@ -189,6 +187,17 @@ export class CodeVerification implements VerificationRecord { + assertThat( + this.verified, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { type, value } = this.identifier; + + return type === 'email' ? { primaryEmail: value } : { primaryPhone: value }; + } + toJson(): CodeVerificationRecordData { const { id, type, identifier, interactionEvent, verified } = this; diff --git a/packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts b/packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts index c282d7635..4a8db8cda 100644 --- a/packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts @@ -21,7 +21,9 @@ import type Queries from '#src/tenants/Queries.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; -import { type VerificationRecord } from './verification-record.js'; +import type { InteractionProfile } from '../../types.js'; + +import { type IdentifierVerificationRecord } from './verification-record.js'; /** The JSON data type for the EnterpriseSsoVerification record stored in the interaction storage */ export type EnterpriseSsoVerificationRecordData = { @@ -44,7 +46,7 @@ export const enterPriseSsoVerificationRecordDataGuard = z.object({ }) satisfies ToZodObject; export class EnterpriseSsoVerification - implements VerificationRecord + implements IdentifierVerificationRecord { static create(libraries: Libraries, queries: Queries, connectorId: string) { return new EnterpriseSsoVerification(libraries, queries, { @@ -81,8 +83,10 @@ export class EnterpriseSsoVerification return Boolean(this.enterpriseSsoUserInfo && this.issuer); } - async getConnectorData(connectorId: string) { - this.connectorDataCache ||= await this.libraries.ssoConnectors.getSsoConnectorById(connectorId); + async getConnectorData() { + this.connectorDataCache ||= await this.libraries.ssoConnectors.getSsoConnectorById( + this.connectorId + ); return this.connectorDataCache; } @@ -104,7 +108,7 @@ export class EnterpriseSsoVerification tenantContext: TenantContext, payload: SocialAuthorizationUrlPayload ) { - const connectorData = await this.getConnectorData(this.connectorId); + const connectorData = await this.getConnectorData(); return getSsoAuthorizationUrl(ctx, tenantContext, connectorData, payload); } @@ -117,7 +121,7 @@ export class EnterpriseSsoVerification * See the above {@link createAuthorizationUrl} method for more details. */ async verify(ctx: WithLogContext, tenantContext: TenantContext, callbackData: JsonObject) { - const connectorData = await this.getConnectorData(this.connectorId); + const connectorData = await this.getConnectorData(); const { issuer, userInfo } = await verifySsoIdentity( ctx, tenantContext, @@ -129,10 +133,14 @@ export class EnterpriseSsoVerification this.enterpriseSsoUserInfo = userInfo; } + /** + * Identify the user by the enterprise SSO identity. + * If the user is not found, find the related user by the enterprise SSO identity and return the related user. + */ async identifyUser(): Promise { assertThat( this.isVerified, - new RequestError({ code: 'session.verification_failed', status: 422 }) + new RequestError({ code: 'session.verification_failed', status: 400 }) ); // TODO: sync userInfo and link sso identity @@ -152,6 +160,49 @@ export class EnterpriseSsoVerification throw new RequestError({ code: 'user.identity_not_exist', status: 404 }); } + /** + * Returns the use SSO identity as a new user profile. + */ + async toUserProfile(): Promise>> { + assertThat( + this.enterpriseSsoUserInfo && this.issuer, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + return { + enterpriseSsoIdentity: { + issuer: this.issuer, + ssoConnectorId: this.connectorId, + identityId: this.enterpriseSsoUserInfo.id, + detail: this.enterpriseSsoUserInfo, + }, + }; + } + + /** + * Returns the synced profile from the enterprise SSO identity. + * + * @param isNewUser - Whether the returned profile is for a new user. If true, the profile should contain the user's email. + */ + async toSyncedProfile( + isNewUser = false + ): Promise> { + assertThat( + this.enterpriseSsoUserInfo && this.issuer, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { name, avatar, email: primaryEmail } = this.enterpriseSsoUserInfo; + + if (isNewUser) { + return { name, avatar, primaryEmail }; + } + + const { syncProfile } = await this.getConnectorData(); + + return syncProfile ? { name, avatar } : {}; + } + toJson(): EnterpriseSsoVerificationRecordData { const { id, connectorId, type, enterpriseSsoUserInfo, issuer } = this; diff --git a/packages/core/src/routes/experience/classes/verifications/password-verification.ts b/packages/core/src/routes/experience/classes/verifications/password-verification.ts index fd9b56751..60d25da7e 100644 --- a/packages/core/src/routes/experience/classes/verifications/password-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/password-verification.ts @@ -15,7 +15,7 @@ import assertThat from '#src/utils/assert-that.js'; import { findUserByIdentifier } from '../utils.js'; -import { type VerificationRecord } from './verification-record.js'; +import { type IdentifierVerificationRecord } from './verification-record.js'; export type PasswordVerificationRecordData = { id: string; @@ -31,7 +31,9 @@ export const passwordVerificationRecordDataGuard = z.object({ verified: z.boolean(), }) satisfies ToZodObject; -export class PasswordVerification implements VerificationRecord { +export class PasswordVerification + implements IdentifierVerificationRecord +{ /** Factory method to create a new `PasswordVerification` record using an identifier */ static create(libraries: Libraries, queries: Queries, identifier: InteractionIdentifier) { return new PasswordVerification(libraries, queries, { @@ -89,7 +91,6 @@ export class PasswordVerification implements VerificationRecord { assertThat( this.verified, @@ -98,7 +99,15 @@ export class PasswordVerification implements VerificationRecord; -export class SocialVerification implements VerificationRecord { +export class SocialVerification implements IdentifierVerificationRecord { /** * Factory method to create a new SocialVerification instance */ @@ -56,6 +59,8 @@ export class SocialVerification implements VerificationRecord { assertThat( this.isVerified, - new RequestError({ code: 'session.verification_failed', status: 422 }) + new RequestError({ code: 'session.verification_failed', status: 400 }) ); // TODO: sync userInfo and link social identity @@ -143,7 +148,7 @@ export class SocialVerification implements VerificationRecord>> { + assertThat( + this.socialUserInfo, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { + metadata: { target }, + } = await this.getConnectorData(); + + return { + socialIdentity: { + target, + userInfo: this.socialUserInfo, + }, + }; + } + + /** + * Returns the synced profile from the social identity. + * + * @param isNewUser - Whether the profile is for a new user. If set to true, the primary email will be included in the profile. + */ + async toSyncedProfile( + isNewUser = false + ): Promise> { + assertThat( + this.socialUserInfo, + new RequestError({ code: 'session.verification_failed', status: 400 }) + ); + + const { name, avatar, email: primaryEmail } = this.socialUserInfo; + + if (isNewUser) { + return { name, avatar, primaryEmail }; + } + + const { + dbEntry: { syncProfile }, + } = await this.getConnectorData(); + + return syncProfile ? { name, avatar } : {}; + } + toJson(): SocialVerificationRecordData { const { id, connectorId, type, socialUserInfo } = this; @@ -166,7 +218,6 @@ export class SocialVerification implements VerificationRecord { - const { socials } = this.libraries; const { users: { findUserByIdentity }, } = this.queries; @@ -177,7 +228,7 @@ export class SocialVerification implements VerificationRecord = { id: string; @@ -17,3 +17,21 @@ export abstract class VerificationRecord< abstract toJson(): Json; } + +type IdentifierVerificationType = + | VerificationType.VerificationCode + | VerificationType.Password + | VerificationType.Social + | VerificationType.EnterpriseSso; + +/** + * The abstract class for all identifier verification records. + * + * - A `identifyUser` method must be provided to identify the user associated with the verification record. + */ +export abstract class IdentifierVerificationRecord< + T extends VerificationType = IdentifierVerificationType, + Json extends Data = Data, +> extends VerificationRecord { + abstract identifyUser(): Promise; +} diff --git a/packages/core/src/routes/experience/index.ts b/packages/core/src/routes/experience/index.ts index f020e06fa..ec83da600 100644 --- a/packages/core/src/routes/experience/index.ts +++ b/packages/core/src/routes/experience/index.ts @@ -79,7 +79,7 @@ export default function experienceApiRoutes( body: z.object({ interactionEvent: z.nativeEnum(InteractionEvent), }), - status: [204, 403], + status: [204, 400, 403], }), async (ctx, next) => { const { interactionEvent } = ctx.guard.body; @@ -107,7 +107,7 @@ export default function experienceApiRoutes( experienceRoutes.identification, koaGuard({ body: identificationApiPayloadGuard, - status: [204, 400, 401, 404, 409], + status: [201, 204, 400, 401, 404, 409, 422], }), async (ctx, next) => { const { verificationId } = ctx.guard.body; diff --git a/packages/core/src/routes/experience/types.ts b/packages/core/src/routes/experience/types.ts index 8100dfcdf..9e0bb69e8 100644 --- a/packages/core/src/routes/experience/types.ts +++ b/packages/core/src/routes/experience/types.ts @@ -1,3 +1,53 @@ +import { type SocialUserInfo, socialUserInfoGuard, type ToZodObject } from '@logto/connector-kit'; +import { type CreateUser, Users, UserSsoIdentities, type UserSsoIdentity } from '@logto/schemas'; import type Provider from 'oidc-provider'; +import { z } from 'zod'; export type Interaction = Awaited>; + +export type InteractionProfile = { + socialIdentity?: { + target: string; + userInfo: SocialUserInfo; + }; + enterpriseSsoIdentity?: Pick< + UserSsoIdentity, + 'identityId' | 'ssoConnectorId' | 'issuer' | 'detail' + >; +} & Pick< + CreateUser, + | 'avatar' + | 'name' + | 'username' + | 'primaryEmail' + | 'primaryPhone' + | 'passwordEncrypted' + | 'passwordEncryptionMethod' +>; + +export const interactionProfileGuard = Users.createGuard + .pick({ + avatar: true, + name: true, + username: true, + primaryEmail: true, + primaryPhone: true, + passwordEncrypted: true, + passwordEncryptionMethod: true, + }) + .extend({ + socialIdentity: z + .object({ + target: z.string(), + userInfo: socialUserInfoGuard, + }) + .optional(), + enterpriseSsoIdentity: UserSsoIdentities.guard + .pick({ + identityId: true, + ssoConnectorId: true, + issuer: true, + detail: true, + }) + .optional(), + }) satisfies ToZodObject;