Skip to content

Commit

Permalink
refactor(experience): migrate the social and sso flow (migration-3) (#…
Browse files Browse the repository at this point in the history
…6406)

* refactor(experience): migrate the social and sso flow

migrate the social and sso flow

* refactor(experience): migrate profile fulfillment flow  (migration-4) (#6414)

* refactor(experience): migrate profile fulfillment flow

migrate the profile fulfillment flow

* refactor(experience): remove unused hook

remove unused hook

* fix(experience): fix password policy checker

fix password policy checker error display

* fix(experience): fix the api name

fix the api name

* refactor(experience): migrate mfa flow (migration-5) (#6417)

* refactor(experience): migrate mfa binding flow

migrate mfa binding flow

* test(experience): update unit tests (migration-6) (#6420)

* test(experience): update unit tests

update unit tests

* chore(experience): remove legacy APIs

remove legacy APIs
  • Loading branch information
simeng-li committed Aug 16, 2024
1 parent bf9346d commit d76df2c
Show file tree
Hide file tree
Showing 70 changed files with 1,272 additions and 915 deletions.
3 changes: 3 additions & 0 deletions packages/experience/src/apis/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import i18next from 'i18next';
import ky from 'ky';

import { kyPrefixUrl } from './const';

export default ky.extend({
prefixUrl: kyPrefixUrl,
hooks: {
beforeRequest: [
(request) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/experience/src/apis/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const consent = async (organizationId?: string) => {
};

return api
.post('/api/interaction/consent', {
.post('api/interaction/consent', {
json: {
organizationIds: organizationId && [organizationId],
},
Expand All @@ -17,5 +17,5 @@ export const consent = async (organizationId?: string) => {
};

export const getConsentInfo = async () => {
return api.get('/api/interaction/consent').json<ConsentInfoResponse>();
return api.get('api/interaction/consent').json<ConsentInfoResponse>();
};
1 change: 1 addition & 0 deletions packages/experience/src/apis/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const kyPrefixUrl = '/';
14 changes: 14 additions & 0 deletions packages/experience/src/apis/experience/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const prefix = 'api/experience';

export const experienceApiRoutes = Object.freeze({
prefix,
identification: `${prefix}/identification`,
submit: `${prefix}/submit`,
verification: `${prefix}/verification`,
profile: `${prefix}/profile`,
mfa: `${prefix}/profile/mfa`,
});

export type VerificationResponse = {
verificationId: string;
};
Original file line number Diff line number Diff line change
@@ -1,60 +1,42 @@
import {
type IdentificationApiPayload,
InteractionEvent,
type PasswordVerificationPayload,
SignInIdentifier,
type UpdateProfileApiPayload,
type VerificationCodeIdentifier,
} from '@logto/schemas';

import api from './api';
import { type ContinueFlowInteractionEvent } from '@/types';

const prefix = '/api/experience';
import api from '../api';

const experienceApiRoutes = Object.freeze({
prefix,
identification: `${prefix}/identification`,
submit: `${prefix}/submit`,
verification: `${prefix}/verification`,
profile: `${prefix}/profile`,
mfa: `${prefix}/profile/mfa`,
});

type VerificationResponse = {
verificationId: string;
};

type SubmitInteractionResponse = {
redirectTo: string;
};

const initInteraction = async (interactionEvent: InteractionEvent) =>
api.put(`${experienceApiRoutes.prefix}`, {
json: {
interactionEvent,
},
});

const identifyUser = async (payload: IdentificationApiPayload = {}) =>
api.post(experienceApiRoutes.identification, { json: payload });

const submitInteraction = async () =>
api.post(`${experienceApiRoutes.submit}`).json<SubmitInteractionResponse>();
import { experienceApiRoutes, type VerificationResponse } from './const';
import {
initInteraction,
identifyUser,
submitInteraction,
updateInteractionEvent,
_updateProfile,
identifyAndSubmitInteraction,
} from './interaction';

export {
initInteraction,
submitInteraction,
identifyUser,
identifyAndSubmitInteraction,
} from './interaction';

export * from './mfa';
export * from './social';

const updateProfile = async (payload: UpdateProfileApiPayload) => {
await api.post(experienceApiRoutes.profile, { json: payload });
export const registerWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.Register);
return identifyAndSubmitInteraction({ verificationId });
};

const updateInteractionEvent = async (interactionEvent: InteractionEvent) =>
api.put(`${experienceApiRoutes.prefix}/interaction-event`, {
json: {
interactionEvent,
},
});

const identifyAndSubmitInteraction = async (payload?: IdentificationApiPayload) => {
await identifyUser(payload);
return submitInteraction();
export const signInWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.SignIn);
return identifyAndSubmitInteraction({ verificationId });
};

// Password APIs
Expand All @@ -73,11 +55,11 @@ export const signInWithPasswordIdentifier = async (payload: PasswordVerification
export const registerWithUsername = async (username: string) => {
await initInteraction(InteractionEvent.Register);

return updateProfile({ type: SignInIdentifier.Username, value: username });
return _updateProfile({ type: SignInIdentifier.Username, value: username });
};

export const continueRegisterWithPassword = async (password: string) => {
await updateProfile({ type: 'password', value: password });
await _updateProfile({ type: 'password', value: password });

return identifyAndSubmitInteraction();
};
Expand Down Expand Up @@ -114,30 +96,45 @@ export const identifyWithVerificationCode = async (json: VerificationCodePayload
return identifyAndSubmitInteraction({ verificationId });
};

export const registerWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.Register);
return identifyAndSubmitInteraction({ verificationId });
};

export const signInWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.SignIn);
return identifyAndSubmitInteraction({ verificationId });
};

// Profile APIs

export const updateProfileWithVerificationCode = async (json: VerificationCodePayload) => {
export const updateProfileWithVerificationCode = async (
json: VerificationCodePayload,
interactionEvent?: ContinueFlowInteractionEvent
) => {
const { verificationId } = await verifyVerificationCode(json);

const {
identifier: { type },
} = json;

await updateProfile({
await _updateProfile({
type,
verificationId,
});

if (interactionEvent === InteractionEvent.Register) {
await identifyUser();
}

return submitInteraction();
};

type UpdateProfilePayload = {
type: SignInIdentifier.Username | 'password';
value: string;
};

export const updateProfile = async (
payload: UpdateProfilePayload,
interactionEvent: ContinueFlowInteractionEvent
) => {
await _updateProfile(payload);

if (interactionEvent === InteractionEvent.Register) {
await identifyUser();
}

return submitInteraction();
};

Expand Down
41 changes: 41 additions & 0 deletions packages/experience/src/apis/experience/interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
type InteractionEvent,
type IdentificationApiPayload,
type UpdateProfileApiPayload,
} from '@logto/schemas';

import api from '../api';

import { experienceApiRoutes } from './const';

type SubmitInteractionResponse = {
redirectTo: string;
};

export const initInteraction = async (interactionEvent: InteractionEvent) =>
api.put(`${experienceApiRoutes.prefix}`, {
json: {
interactionEvent,
},
});

export const identifyUser = async (payload: IdentificationApiPayload = {}) =>
api.post(experienceApiRoutes.identification, { json: payload });

export const submitInteraction = async () =>
api.post(`${experienceApiRoutes.submit}`).json<SubmitInteractionResponse>();

export const _updateProfile = async (payload: UpdateProfileApiPayload) =>
api.post(experienceApiRoutes.profile, { json: payload });

export const updateInteractionEvent = async (interactionEvent: InteractionEvent) =>
api.put(`${experienceApiRoutes.prefix}/interaction-event`, {
json: {
interactionEvent,
},
});

export const identifyAndSubmitInteraction = async (payload?: IdentificationApiPayload) => {
await identifyUser(payload);
return submitInteraction();
};
129 changes: 129 additions & 0 deletions packages/experience/src/apis/experience/mfa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {
MfaFactor,
type WebAuthnRegistrationOptions,
type WebAuthnAuthenticationOptions,
type BindMfaPayload,
type VerifyMfaPayload,
} from '@logto/schemas';

import api from '../api';

import { experienceApiRoutes } from './const';
import { submitInteraction } from './interaction';

/**
* Mfa APIs
*/
const addMfa = async (type: MfaFactor, verificationId: string) =>
api.post(`${experienceApiRoutes.mfa}`, {
json: {
type,
verificationId,
},
});

type TotpSecretResponse = {
verificationId: string;
secret: string;
secretQrCode: string;
};
export const createTotpSecret = async () =>
api.post(`${experienceApiRoutes.verification}/totp/secret`).json<TotpSecretResponse>();

export const createWebAuthnRegistration = async () => {
const { verificationId, registrationOptions } = await api
.post(`${experienceApiRoutes.verification}/web-authn/registration`)
.json<{ verificationId: string; registrationOptions: WebAuthnRegistrationOptions }>();

return {
verificationId,
options: registrationOptions,
};
};

export const createWebAuthnAuthentication = async () => {
const { verificationId, authenticationOptions } = await api
.post(`${experienceApiRoutes.verification}/web-authn/authentication`)
.json<{ verificationId: string; authenticationOptions: WebAuthnAuthenticationOptions }>();

return {
verificationId,
options: authenticationOptions,
};
};

export const createBackupCode = async () =>
api.post(`${experienceApiRoutes.verification}/backup-code/generate`).json<{
verificationId: string;
codes: string[];
}>();

export const skipMfa = async () => {
await api.post(`${experienceApiRoutes.mfa}/mfa-skipped`);
return submitInteraction();
};

export const bindMfa = async (payload: BindMfaPayload, verificationId: string) => {
switch (payload.type) {
case MfaFactor.TOTP: {
const { code } = payload;
await api.post(`${experienceApiRoutes.verification}/totp/verify`, {
json: {
code,
verificationId,
},
});
break;
}
case MfaFactor.WebAuthn: {
await api.post(`${experienceApiRoutes.verification}/web-authn/registration/verify`, {
json: {
verificationId,
payload,
},
});
break;
}
case MfaFactor.BackupCode: {
// No need to verify backup codes
break;
}
}

await addMfa(payload.type, verificationId);
return submitInteraction();
};

export const verifyMfa = async (payload: VerifyMfaPayload, verificationId?: string) => {
switch (payload.type) {
case MfaFactor.TOTP: {
const { code } = payload;
await api.post(`${experienceApiRoutes.verification}/totp/verify`, {
json: {
code,
},
});
break;
}
case MfaFactor.WebAuthn: {
await api.post(`${experienceApiRoutes.verification}/web-authn/authentication/verify`, {
json: {
verificationId,
payload,
},
});
break;
}
case MfaFactor.BackupCode: {
const { code } = payload;
await api.post(`${experienceApiRoutes.verification}/backup-code/verify`, {
json: {
code,
},
});
break;
}
}

return submitInteraction();
};
Loading

0 comments on commit d76df2c

Please sign in to comment.