0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

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
This commit is contained in:
simeng-li 2024-07-15 19:02:57 +08:00 committed by GitHub
parent f94fb519f4
commit 84ac935c80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 446 additions and 70 deletions

View file

@ -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<string, unknown>;
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<InteractionStorage>;
@ -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<VerificationType, VerificationRecord>();
@ -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;
}
}

View file

@ -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<InteractionProfile> => {
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<InteractionProfile>['socialIdentity']
): User['identities'] => {
const { target, userInfo } = socialIdentity;
return {
[target]: {
userId: userInfo.id,
details: userInfo,
},
};
};

View file

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

View file

@ -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<InteractionEvent, TemplateType> = {
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<VerificationType.VerificationCode> {
export class CodeVerification
implements IdentifierVerificationRecord<VerificationType.VerificationCode>
{
/**
* Factory method to create a new CodeVerification record using the given identifier.
*/
@ -164,10 +166,6 @@ export class CodeVerification implements VerificationRecord<VerificationType.Ver
this.verified = true;
}
/**
* Identify the user by the current `identifier`.
* Return undefined if the verification record is not verified or no user is found by the identifier.
*/
async identifyUser(): Promise<User> {
assertThat(
this.verified,
@ -189,6 +187,17 @@ export class CodeVerification implements VerificationRecord<VerificationType.Ver
return user;
}
async toUserProfile(): Promise<{ primaryEmail: string } | { primaryPhone: string }> {
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;

View file

@ -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<EnterpriseSsoVerificationRecordData>;
export class EnterpriseSsoVerification
implements VerificationRecord<VerificationType.EnterpriseSso>
implements IdentifierVerificationRecord<VerificationType.EnterpriseSso>
{
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<User> {
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<Required<Pick<InteractionProfile, 'enterpriseSsoIdentity'>>> {
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<Pick<InteractionProfile, 'avatar' | 'name' | 'primaryEmail'>> {
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;

View file

@ -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<PasswordVerificationRecordData>;
export class PasswordVerification implements VerificationRecord<VerificationType.Password> {
export class PasswordVerification
implements IdentifierVerificationRecord<VerificationType.Password>
{
/** 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<VerificationType
return user;
}
/** Identifies the user using the username */
async identifyUser(): Promise<User> {
assertThat(
this.verified,
@ -98,7 +99,15 @@ export class PasswordVerification implements VerificationRecord<VerificationType
const user = await findUserByIdentifier(this.queries.users, this.identifier);
assertThat(user, new RequestError({ code: 'user.user_not_exist', status: 404 }));
assertThat(
user,
new RequestError(
{ code: 'user.user_not_exist', status: 404 },
{
identifier: this.identifier.value,
}
)
);
return user;
}

View file

@ -18,8 +18,11 @@ import type Libraries from '#src/tenants/Libraries.js';
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 LogtoConnector } from '#src/utils/connectors/types.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 SocialVerification record stored in the interaction storage */
export type SocialVerificationRecordData = {
@ -39,7 +42,7 @@ export const socialVerificationRecordDataGuard = z.object({
socialUserInfo: socialUserInfoGuard.optional(),
}) satisfies ToZodObject<SocialVerificationRecordData>;
export class SocialVerification implements VerificationRecord<VerificationType.Social> {
export class SocialVerification implements IdentifierVerificationRecord<VerificationType.Social> {
/**
* Factory method to create a new SocialVerification instance
*/
@ -56,6 +59,8 @@ export class SocialVerification implements VerificationRecord<VerificationType.S
public readonly connectorId: string;
public socialUserInfo?: SocialUserInfo;
private connectorDataCache?: LogtoConnector;
constructor(
private readonly libraries: Libraries,
private readonly queries: Queries,
@ -130,7 +135,7 @@ export class SocialVerification implements VerificationRecord<VerificationType.S
async identifyUser(): Promise<User> {
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<VerificationType.S
throw new RequestError(
{
code: 'user.identity_not_exist',
status: 422,
status: 404,
},
{
...(relatedUser && { relatedUser: relatedUser[0] }),
@ -154,6 +159,53 @@ export class SocialVerification implements VerificationRecord<VerificationType.S
return user;
}
/**
* Returns the social identity as a new user profile.
*/
async toUserProfile(): Promise<Required<Pick<InteractionProfile, 'socialIdentity'>>> {
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<Pick<InteractionProfile, 'avatar' | 'name' | 'primaryEmail'>> {
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<VerificationType.S
}
private async findUserBySocialIdentity(): Promise<User | undefined> {
const { socials } = this.libraries;
const {
users: { findUserByIdentity },
} = this.queries;
@ -177,7 +228,7 @@ export class SocialVerification implements VerificationRecord<VerificationType.S
const {
metadata: { target },
} = await socials.getConnector(this.connectorId);
} = await this.getConnectorData();
const user = await findUserByIdentity(target, this.socialUserInfo.id);
@ -198,4 +249,12 @@ export class SocialVerification implements VerificationRecord<VerificationType.S
return socials.findSocialRelatedUser(this.socialUserInfo);
}
private async getConnectorData() {
const { getConnector } = this.libraries.socials;
this.connectorDataCache ||= await getConnector(this.connectorId);
return this.connectorDataCache;
}
}

View file

@ -1,4 +1,4 @@
import type { VerificationType } from '@logto/schemas';
import { type User, type VerificationType } from '@logto/schemas';
type Data<T> = {
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<T> = Data<T>,
> extends VerificationRecord<T, Json> {
abstract identifyUser(): Promise<User>;
}

View file

@ -79,7 +79,7 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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;

View file

@ -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<ReturnType<Provider['interactionDetails']>>;
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<InteractionProfile>;