Repository: logto-io/logto
File: packages/core/src/routes/experience/profile-routes.ts (TypeScript)

Code before:
 * @throws {RequestError} with status 404 if current interaction is not identified
 * @throws {RequestError} with status 403 if MFA verification status is not verified
 */
function verifiedInteractionGuard<StateT, ContextT extends WithLogContext, ResponseT>(options?: {
  allowMissingMfa?: boolean;
}): MiddlewareType<StateT, ExperienceInteractionRouterContext<ContextT>, ResponseT> {
  return async (ctx, next) => {
    const { experienceInteraction } = ctx;

    // Guard current interaction event is not ForgotPassword
    assertThat(
      experienceInteraction.interactionEvent !== InteractionEvent.ForgotPassword,
      new RequestError({
        code: 'session.not_supported_for_forgot_password',
        statue: 400,
      })
    );

    if (experienceInteraction.interactionEvent === InteractionEvent.SignIn) {
      try {
        await experienceInteraction.guardMfaVerificationStatus();
      } catch (error: unknown) {
        if (
          options?.allowMissingMfa &&
          error instanceof RequestError &&
          error.code === 'user.missing_mfa'
        ) {
          return next();
        }

        throw error;
      }
    }

    return next();
  };
}

export default function interactionProfileRoutes<T extends ExperienceInteractionRouterContext>(
  router: Router<unknown, T>,
  { libraries, queries }: TenantContext
) {
  router.post(
    `${experienceRoutes.profile}`,
    koaGuard({
      body: updateProfileApiPayloadGuard,
      status: [204, 400, 403, 404, 422],
    }),
    async (ctx, next) => {
      const { experienceInteraction, guard, createLog } = ctx;
      const profilePayload = guard.body;

Review comment (suggestion):
Ditto, `guardMfaVerificationStatus` should not throw `allowMissingMfa` error.  Experience interaction follows the following life cycle
1. Identify the user
2. Guard mfa verificatiron status on any existing enabled MFA factors
3. Guard any missing profile
4. Guard any missing mfa


Based on this design, `guardMfaVerificationStatus` should pass if no existing enabled MFA factors are found. Then guard the missing MFA on interaction submission at the following steps. 


Code after:
 * @throws {RequestError} with status 404 if current interaction is not identified
 * @throws {RequestError} with status 403 if MFA verification status is not verified
 */
function verifiedInteractionGuard<
  StateT,
  ContextT extends WithLogContext,
  ResponseT,
>(): MiddlewareType<StateT, ExperienceInteractionRouterContext<ContextT>, ResponseT> {
  return async (ctx, next) => {
    const { experienceInteraction } = ctx;

    // Guard current interaction event is not ForgotPassword
    assertThat(
      experienceInteraction.interactionEvent !== InteractionEvent.ForgotPassword,
      new RequestError({
        code: 'session.not_supported_for_forgot_password',
        statue: 400,
      })
    );

    if (experienceInteraction.interactionEvent === InteractionEvent.SignIn) {
      await experienceInteraction.guardMfaVerificationStatus();
    }

    return next();
  };
}

export default function interactionProfileRoutes<T extends ExperienceInteractionRouterContext>(
  router: Router<unknown, T>,
  { libraries, queries }: TenantContext
) {
  router.post(
    `${experienceRoutes.profile}`,
    koaGuard({
      body: updateProfileApiPayloadGuard,
      status: [204, 400, 403, 404, 422],
    }),
    async (ctx, next) => {
      const { experienceInteraction, guard, createLog } = ctx;
      const profilePayload = guard.body;
      const { interactionEvent } = experienceInteraction;

      const log = createLog(`Interaction.${interactionEvent}.Profile.Update`);

      // Guard current interaction event is not ForgotPassword
      assertThat(
        interactionEvent !== InteractionEvent.ForgotPassword,
        new RequestError({
          code: 'session.not_supported_for_forgot_password',
          statue: 400,