mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
refactor(core): sign-in and register flow share verification session (#2109)
This commit is contained in:
parent
9dba4b14a0
commit
2c0c49e1fb
5 changed files with 120 additions and 68 deletions
|
@ -1,2 +1 @@
|
|||
export const forgotPasswordVerificationTimeout = 10 * 60; // 10 mins.
|
||||
export const verificationTimeout = 10 * 60; // 10 mins.
|
||||
|
|
|
@ -337,7 +337,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('should call interactionResult', async () => {
|
||||
it('should call interactionResult (with flow `sign-in`)', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
|
@ -358,6 +358,27 @@ describe('session -> passwordlessRoutes', () => {
|
|||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('should call interactionResult (with flow `register`)', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
phone: '13000000000',
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest.post(`${signInRoute}/sms`);
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
login: { accountId: 'id' },
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('throw when verification session invalid', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
|
@ -370,12 +391,12 @@ describe('session -> passwordlessRoutes', () => {
|
|||
const response = await sessionRequest.post(`${signInRoute}/sms`);
|
||||
expect(response.statusCode).toEqual(404);
|
||||
});
|
||||
it('throw when flow is not `sign-in`', async () => {
|
||||
it('throw when flow is not `sign-in` and `register`', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
phone: '13000000000',
|
||||
flow: PasscodeType.Register,
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
},
|
||||
},
|
||||
|
@ -440,7 +461,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('should call interactionResult', async () => {
|
||||
it('should call interactionResult (with flow `sign-in`)', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
|
@ -461,6 +482,27 @@ describe('session -> passwordlessRoutes', () => {
|
|||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('should call interactionResult (with flow `register`)', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
email: 'a@a.com',
|
||||
flow: PasscodeType.Register,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest.post(`${signInRoute}/email`);
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
login: { accountId: 'id' },
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('throw when verification session invalid', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
|
@ -473,12 +515,12 @@ describe('session -> passwordlessRoutes', () => {
|
|||
const response = await sessionRequest.post(`${signInRoute}/email`);
|
||||
expect(response.statusCode).toEqual(404);
|
||||
});
|
||||
it('throw when flow is not `sign-in`', async () => {
|
||||
it('throw when flow is not `sign-in` and `register`', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
email: 'a@a.com',
|
||||
flow: PasscodeType.Register,
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
},
|
||||
},
|
||||
|
@ -517,7 +559,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('should call interactionResult', async () => {
|
||||
it('should call interactionResult (with flow `register`)', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
|
@ -538,6 +580,27 @@ describe('session -> passwordlessRoutes', () => {
|
|||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('should call interactionResult (with flow `sign-in`)', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
phone: '13000000001',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest.post(`${registerRoute}/sms`);
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
login: { accountId: 'user1' },
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('throw when verification session invalid', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
|
@ -550,12 +613,12 @@ describe('session -> passwordlessRoutes', () => {
|
|||
const response = await sessionRequest.post(`${registerRoute}/sms`);
|
||||
expect(response.statusCode).toEqual(404);
|
||||
});
|
||||
it('throw when flow is not `register`', async () => {
|
||||
it('throw when flow is not `register` and `sign-in`', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
phone: '13000000001',
|
||||
flow: PasscodeType.SignIn,
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
},
|
||||
},
|
||||
|
@ -594,7 +657,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('should call interactionResult', async () => {
|
||||
it('should call interactionResult (with flow `register`)', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
|
@ -615,6 +678,27 @@ describe('session -> passwordlessRoutes', () => {
|
|||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('should call interactionResult (with flow `sign-in`)', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
email: 'b@a.com',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest.post(`${registerRoute}/email`);
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
login: { accountId: 'user1' },
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it('throw when verification session invalid', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
|
@ -627,12 +711,12 @@ describe('session -> passwordlessRoutes', () => {
|
|||
const response = await sessionRequest.post(`${registerRoute}/email`);
|
||||
expect(response.statusCode).toEqual(404);
|
||||
});
|
||||
it('throw when flow is not `register`', async () => {
|
||||
it('throw when flow is not `register` and `sign-in`', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
email: 'b@a.com',
|
||||
flow: PasscodeType.SignIn,
|
||||
flow: PasscodeType.ForgotPassword,
|
||||
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -16,11 +16,9 @@ import {
|
|||
updateUserById,
|
||||
} from '@/queries/user';
|
||||
import {
|
||||
emailRegisterSessionResultGuard,
|
||||
emailSignInSessionResultGuard,
|
||||
emailSessionResultGuard,
|
||||
passcodeTypeGuard,
|
||||
smsRegisterSessionResultGuard,
|
||||
smsSignInSessionResultGuard,
|
||||
smsSessionResultGuard,
|
||||
} from '@/routes/session/types';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
|
@ -178,7 +176,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
const verificationStorage = await getVerificationStorageFromInteraction(
|
||||
ctx,
|
||||
provider,
|
||||
smsSignInSessionResultGuard
|
||||
smsSessionResultGuard
|
||||
);
|
||||
|
||||
const type = getPasswordlessRelatedLogType(PasscodeType.SignIn, 'sms');
|
||||
|
@ -205,7 +203,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
const verificationStorage = await getVerificationStorageFromInteraction(
|
||||
ctx,
|
||||
provider,
|
||||
emailSignInSessionResultGuard
|
||||
emailSessionResultGuard
|
||||
);
|
||||
|
||||
const type = getPasswordlessRelatedLogType(PasscodeType.SignIn, 'email');
|
||||
|
@ -232,7 +230,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
const verificationStorage = await getVerificationStorageFromInteraction(
|
||||
ctx,
|
||||
provider,
|
||||
smsRegisterSessionResultGuard
|
||||
smsSessionResultGuard
|
||||
);
|
||||
|
||||
const type = getPasswordlessRelatedLogType(PasscodeType.Register, 'sms');
|
||||
|
@ -259,7 +257,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
const verificationStorage = await getVerificationStorageFromInteraction(
|
||||
ctx,
|
||||
provider,
|
||||
emailRegisterSessionResultGuard
|
||||
emailSessionResultGuard
|
||||
);
|
||||
|
||||
const type = getPasswordlessRelatedLogType(PasscodeType.Register, 'email');
|
||||
|
|
|
@ -14,58 +14,32 @@ export type Operation = z.infer<typeof operationGuard>;
|
|||
export type VerifiedIdentity = { email: string } | { phone: string } | { id: string };
|
||||
|
||||
export type VerificationStorage =
|
||||
| SmsSignInSessionStorage
|
||||
| EmailSignInSessionStorage
|
||||
| SmsRegisterSessionStorage
|
||||
| EmailRegisterSessionStorage
|
||||
| SmsSessionStorage
|
||||
| EmailSessionStorage
|
||||
| ForgotPasswordSessionStorage;
|
||||
|
||||
export type VerificationResult<T = VerificationStorage> = { verification: T };
|
||||
|
||||
const smsSignInSessionStorageGuard = z.object({
|
||||
flow: z.literal(PasscodeType.SignIn),
|
||||
const smsSessionStorageGuard = z.object({
|
||||
flow: z.literal(PasscodeType.SignIn).or(z.literal(PasscodeType.Register)),
|
||||
expiresAt: z.string(),
|
||||
phone: z.string(),
|
||||
});
|
||||
|
||||
export type SmsSignInSessionStorage = z.infer<typeof smsSignInSessionStorageGuard>;
|
||||
export type SmsSessionStorage = z.infer<typeof smsSessionStorageGuard>;
|
||||
|
||||
export const smsSignInSessionResultGuard = z.object({ verification: smsSignInSessionStorageGuard });
|
||||
export const smsSessionResultGuard = z.object({ verification: smsSessionStorageGuard });
|
||||
|
||||
const emailSignInSessionStorageGuard = z.object({
|
||||
flow: z.literal(PasscodeType.SignIn),
|
||||
const emailSessionStorageGuard = z.object({
|
||||
flow: z.literal(PasscodeType.SignIn).or(z.literal(PasscodeType.Register)),
|
||||
expiresAt: z.string(),
|
||||
email: z.string(),
|
||||
});
|
||||
|
||||
export type EmailSignInSessionStorage = z.infer<typeof emailSignInSessionStorageGuard>;
|
||||
export type EmailSessionStorage = z.infer<typeof emailSessionStorageGuard>;
|
||||
|
||||
export const emailSignInSessionResultGuard = z.object({
|
||||
verification: emailSignInSessionStorageGuard,
|
||||
});
|
||||
|
||||
const smsRegisterSessionStorageGuard = z.object({
|
||||
flow: z.literal(PasscodeType.Register),
|
||||
expiresAt: z.string(),
|
||||
phone: z.string(),
|
||||
});
|
||||
|
||||
export type SmsRegisterSessionStorage = z.infer<typeof smsRegisterSessionStorageGuard>;
|
||||
|
||||
export const smsRegisterSessionResultGuard = z.object({
|
||||
verification: smsRegisterSessionStorageGuard,
|
||||
});
|
||||
|
||||
const emailRegisterSessionStorageGuard = z.object({
|
||||
flow: z.literal(PasscodeType.Register),
|
||||
expiresAt: z.string(),
|
||||
email: z.string(),
|
||||
});
|
||||
|
||||
export type EmailRegisterSessionStorage = z.infer<typeof emailRegisterSessionStorageGuard>;
|
||||
|
||||
export const emailRegisterSessionResultGuard = z.object({
|
||||
verification: emailRegisterSessionStorageGuard,
|
||||
export const emailSessionResultGuard = z.object({
|
||||
verification: emailSessionStorageGuard,
|
||||
});
|
||||
|
||||
const forgotPasswordSessionStorageGuard = z.object({
|
||||
|
|
|
@ -10,14 +10,13 @@ import assertThat from '@/utils/assert-that';
|
|||
|
||||
import { verificationTimeout } from './consts';
|
||||
import {
|
||||
emailRegisterSessionResultGuard,
|
||||
emailSignInSessionResultGuard,
|
||||
emailSessionResultGuard,
|
||||
smsSessionResultGuard,
|
||||
forgotPasswordSessionResultGuard,
|
||||
Method,
|
||||
Operation,
|
||||
smsRegisterSessionResultGuard,
|
||||
smsSignInSessionResultGuard,
|
||||
VerificationResult,
|
||||
VerificationStorage,
|
||||
VerifiedIdentity,
|
||||
} from './types';
|
||||
|
||||
|
@ -45,7 +44,7 @@ export const getPasswordlessRelatedLogType = (
|
|||
return result.data;
|
||||
};
|
||||
|
||||
const parseVerificationStorage = <T = unknown>(
|
||||
const parseVerificationStorage = <T = VerificationStorage>(
|
||||
data: unknown,
|
||||
resultGuard: ZodType<VerificationResult<T>>
|
||||
): T => {
|
||||
|
@ -64,7 +63,7 @@ const parseVerificationStorage = <T = unknown>(
|
|||
return verificationResult.data.verification;
|
||||
};
|
||||
|
||||
export const getVerificationStorageFromInteraction = async <T = unknown>(
|
||||
export const getVerificationStorageFromInteraction = async <T = VerificationStorage>(
|
||||
ctx: Context,
|
||||
provider: Provider,
|
||||
resultGuard: ZodType<VerificationResult<T>>
|
||||
|
@ -96,10 +95,8 @@ export const assignVerificationResult = async (
|
|||
};
|
||||
|
||||
assertThat(
|
||||
smsSignInSessionResultGuard.safeParse(verificationResult).success ||
|
||||
emailSignInSessionResultGuard.safeParse(verificationResult).success ||
|
||||
smsRegisterSessionResultGuard.safeParse(verificationResult).success ||
|
||||
emailRegisterSessionResultGuard.safeParse(verificationResult).success ||
|
||||
emailSessionResultGuard.safeParse(verificationResult).success ||
|
||||
smsSessionResultGuard.safeParse(verificationResult).success ||
|
||||
forgotPasswordSessionResultGuard.safeParse(verificationResult).success,
|
||||
new RequestError({ code: 'session.invalid_verification' })
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue