mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
feat(core): add POST /session/forgot-password/{email,sms}/send-passcode (#1963)
This commit is contained in:
parent
f97ec56fbf
commit
af2600d828
5 changed files with 139 additions and 1 deletions
70
packages/core/src/routes/session/forgot-password.test.ts
Normal file
70
packages/core/src/routes/session/forgot-password.test.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import forgotPasswordRoutes, { forgotPasswordRoute } from './forgot-password';
|
||||
|
||||
const sendPasscode = jest.fn(async () => ({ dbEntry: { id: 'connectorIdValue' } }));
|
||||
jest.mock('@/lib/passcode', () => ({
|
||||
createPasscode: async () => ({ id: 'id' }),
|
||||
sendPasscode: async () => sendPasscode(),
|
||||
}));
|
||||
|
||||
const interactionResult = jest.fn(async () => 'redirectTo');
|
||||
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails,
|
||||
interactionResult,
|
||||
})),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
interactionResult.mockClear();
|
||||
});
|
||||
|
||||
describe('session -> forgotPasswordRoutes', () => {
|
||||
const sessionRequest = createRequester({
|
||||
anonymousRoutes: forgotPasswordRoutes,
|
||||
provider: new Provider(''),
|
||||
middlewares: [
|
||||
async (ctx, next) => {
|
||||
ctx.addLogContext = jest.fn();
|
||||
ctx.log = jest.fn();
|
||||
|
||||
return next();
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('POST /session/forgot-password/sms/send-passcode', () => {
|
||||
beforeAll(() => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
jti: 'jti',
|
||||
});
|
||||
});
|
||||
it('should call sendPasscode', async () => {
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/sms/send-passcode`)
|
||||
.send({ phone: '13000000000' });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(sendPasscode).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/forgot-password/email/send-passcode', () => {
|
||||
beforeAll(() => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
jti: 'jti',
|
||||
});
|
||||
});
|
||||
it('should call sendPasscode', async () => {
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/email/send-passcode`)
|
||||
.send({ email: 'a@a.com' });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(sendPasscode).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
53
packages/core/src/routes/session/forgot-password.ts
Normal file
53
packages/core/src/routes/session/forgot-password.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { emailRegEx, phoneRegEx } from '@logto/core-kit';
|
||||
import { PasscodeType } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createPasscode, sendPasscode } from '@/lib/passcode';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
|
||||
import { AnonymousRouter } from '../types';
|
||||
import { getRoutePrefix } from './utils';
|
||||
|
||||
export const forgotPasswordRoute = getRoutePrefix('forgot-password');
|
||||
|
||||
export default function forgotPasswordRoutes<T extends AnonymousRouter>(
|
||||
router: T,
|
||||
provider: Provider
|
||||
) {
|
||||
router.post(
|
||||
`${forgotPasswordRoute}/sms/send-passcode`,
|
||||
koaGuard({ body: z.object({ phone: z.string().regex(phoneRegEx) }) }),
|
||||
async (ctx, next) => {
|
||||
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const { phone } = ctx.guard.body;
|
||||
const type = 'ForgotPasswordSmsSendPasscode';
|
||||
ctx.log(type, { phone });
|
||||
|
||||
const passcode = await createPasscode(jti, PasscodeType.ForgotPassword, { phone });
|
||||
const { dbEntry } = await sendPasscode(passcode);
|
||||
ctx.log(type, { connectorId: dbEntry.id });
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${forgotPasswordRoute}/email/send-passcode`,
|
||||
koaGuard({ body: z.object({ email: z.string().regex(emailRegEx) }) }),
|
||||
async (ctx, next) => {
|
||||
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const { email } = ctx.guard.body;
|
||||
const type = 'ForgotPasswordEmailSendPasscode';
|
||||
ctx.log(type, { email });
|
||||
|
||||
const passcode = await createPasscode(jti, PasscodeType.ForgotPassword, { email });
|
||||
const { dbEntry } = await sendPasscode(passcode);
|
||||
ctx.log(type, { connectorId: dbEntry.id });
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -10,6 +10,7 @@ import { assignInteractionResults, saveUserFirstConsentedAppId } from '@/lib/ses
|
|||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
import { AnonymousRouter } from '../types';
|
||||
import forgotPasswordRoutes from './forgot-password';
|
||||
import koaGuardSessionAction from './middleware/koa-guard-session-action';
|
||||
import passwordlessRoutes from './passwordless';
|
||||
import socialRoutes from './social';
|
||||
|
@ -87,4 +88,6 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
usernamePasswordRoutes(router, provider);
|
||||
passwordlessRoutes(router, provider);
|
||||
socialRoutes(router, provider);
|
||||
|
||||
forgotPasswordRoutes(router, provider);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Truthy } from '@silverhand/essentials';
|
||||
|
||||
export const getRoutePrefix = (
|
||||
type: 'sign-in' | 'register',
|
||||
type: 'sign-in' | 'register' | 'forgot-password',
|
||||
method?: 'passwordless' | 'username-password' | 'social'
|
||||
) => {
|
||||
return ['session', type, method]
|
||||
|
|
|
@ -96,6 +96,16 @@ type SignInSocialLogPayload = SignInSocialBindLogPayload & {
|
|||
redirectTo?: string;
|
||||
};
|
||||
|
||||
type ForgotPasswordSmsSendPasscodeLogPayload = ArbitraryLogPayload & {
|
||||
phone?: string;
|
||||
connectorId?: string;
|
||||
};
|
||||
|
||||
type ForgotPasswordEmailSendPasscodeLogPayload = ArbitraryLogPayload & {
|
||||
email?: string;
|
||||
connectorId?: string;
|
||||
};
|
||||
|
||||
export enum TokenType {
|
||||
AccessToken = 'AccessToken',
|
||||
RefreshToken = 'RefreshToken',
|
||||
|
@ -131,6 +141,8 @@ export type LogPayloads = {
|
|||
SignInSms: SignInSmsLogPayload;
|
||||
SignInSocialBind: SignInSocialBindLogPayload;
|
||||
SignInSocial: SignInSocialLogPayload;
|
||||
ForgotPasswordSmsSendPasscode: ForgotPasswordSmsSendPasscodeLogPayload;
|
||||
ForgotPasswordEmailSendPasscode: ForgotPasswordEmailSendPasscodeLogPayload;
|
||||
CodeExchangeToken: ExchangeTokenLogPayload;
|
||||
RefreshTokenExchangeToken: ExchangeTokenLogPayload;
|
||||
RevokeToken: RevokeTokenLogPayload;
|
||||
|
|
Loading…
Add table
Reference in a new issue