mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
feat(core): password sign in with phone or email (#2266)
This commit is contained in:
parent
3e6021ad16
commit
415c24aace
7 changed files with 356 additions and 98 deletions
|
@ -1,10 +1,9 @@
|
||||||
import type { User } from '@logto/schemas';
|
import type { User } from '@logto/schemas';
|
||||||
import { UserRole, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
|
import { UserRole, SignUpIdentifier } from '@logto/schemas';
|
||||||
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
|
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
|
||||||
import { Provider } from 'oidc-provider';
|
import { Provider } from 'oidc-provider';
|
||||||
|
|
||||||
import { mockSignInExperience, mockSignInMethod, mockUser } from '@/__mocks__';
|
import { mockSignInExperience, mockUser } from '@/__mocks__';
|
||||||
import RequestError from '@/errors/RequestError';
|
|
||||||
import { createRequester } from '@/utils/test-utils';
|
import { createRequester } from '@/utils/test-utils';
|
||||||
|
|
||||||
import passwordRoutes, { registerRoute, signInRoute } from './password';
|
import passwordRoutes, { registerRoute, signInRoute } from './password';
|
||||||
|
@ -13,13 +12,7 @@ const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||||
const hasActiveUsers = jest.fn(async () => true);
|
const hasActiveUsers = jest.fn(async () => true);
|
||||||
const findDefaultSignInExperience = jest.fn(async () => ({
|
const findDefaultSignInExperience = jest.fn(async () => mockSignInExperience);
|
||||||
...mockSignInExperience,
|
|
||||||
signUp: {
|
|
||||||
...mockSignInExperience.signUp,
|
|
||||||
identifier: SignUpIdentifier.Username,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@/queries/user', () => ({
|
jest.mock('@/queries/user', () => ({
|
||||||
findUserById: async () => findUserById(),
|
findUserById: async () => findUserById(),
|
||||||
|
@ -36,7 +29,7 @@ jest.mock('@/queries/user', () => ({
|
||||||
async findUserByUsername(username: string) {
|
async findUserByUsername(username: string) {
|
||||||
const roleNames = username === 'admin' ? [UserRole.Admin] : [];
|
const roleNames = username === 'admin' ? [UserRole.Admin] : [];
|
||||||
|
|
||||||
return { id: 'user1', username, roleNames };
|
return { id: 'id', username, roleNames };
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -45,17 +38,7 @@ jest.mock('@/queries/sign-in-experience', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@/lib/user', () => ({
|
jest.mock('@/lib/user', () => ({
|
||||||
async verifyUserPassword(user: User, password: string) {
|
async verifyUserPassword(user: User) {
|
||||||
const { username } = user;
|
|
||||||
|
|
||||||
if (username !== 'username' && username !== 'admin') {
|
|
||||||
throw new RequestError('session.invalid_credentials');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password !== 'password') {
|
|
||||||
throw new RequestError('session.invalid_credentials');
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
generateUserId: () => 'user1',
|
generateUserId: () => 'user1',
|
||||||
|
@ -116,60 +99,52 @@ describe('session -> password routes', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /session/sign-in/password/username', () => {
|
it('POST /session/sign-in/password/username', async () => {
|
||||||
it('assign result and redirect', async () => {
|
interactionDetails.mockResolvedValueOnce({ params: {} });
|
||||||
interactionDetails.mockResolvedValueOnce({ params: {} });
|
const response = await sessionRequest.post(`${signInRoute}/username`).send({
|
||||||
const response = await sessionRequest.post(`${signInRoute}/username`).send({
|
username: 'username',
|
||||||
username: 'username',
|
password: 'password',
|
||||||
password: 'password',
|
|
||||||
});
|
|
||||||
expect(response.statusCode).toEqual(200);
|
|
||||||
expect(response.body).toHaveProperty('redirectTo');
|
|
||||||
expect(interactionResult).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.objectContaining({ login: { accountId: 'user1' } }),
|
|
||||||
expect.anything()
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
expect(response.statusCode).toEqual(200);
|
||||||
|
expect(response.body).toHaveProperty('redirectTo');
|
||||||
|
expect(interactionResult).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({ login: { accountId: 'id' } }),
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('throw if user not found', async () => {
|
it('POST /session/sign-in/password/email', async () => {
|
||||||
interactionDetails.mockResolvedValueOnce({ params: {} });
|
interactionDetails.mockResolvedValueOnce({ params: {} });
|
||||||
const response = await sessionRequest.post(`${signInRoute}/username`).send({
|
const response = await sessionRequest.post(`${signInRoute}/email`).send({
|
||||||
username: 'notexistuser',
|
email: 'email',
|
||||||
password: 'password',
|
password: 'password',
|
||||||
});
|
|
||||||
expect(response.statusCode).toEqual(400);
|
|
||||||
});
|
});
|
||||||
|
expect(response.statusCode).toEqual(200);
|
||||||
|
expect(response.body).toHaveProperty('redirectTo');
|
||||||
|
expect(interactionResult).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({ login: { accountId: 'id' } }),
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('throw if user found but wrong password', async () => {
|
it('POST /session/sign-in/password/sms', async () => {
|
||||||
interactionDetails.mockResolvedValueOnce({ params: {} });
|
interactionDetails.mockResolvedValueOnce({ params: {} });
|
||||||
const response = await sessionRequest.post(`${signInRoute}/username`).send({
|
const response = await sessionRequest.post(`${signInRoute}/sms`).send({
|
||||||
username: 'username',
|
phone: 'phone',
|
||||||
password: '_password',
|
password: 'password',
|
||||||
});
|
|
||||||
expect(response.statusCode).toEqual(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throw if sign in method is not enabled', async () => {
|
|
||||||
findDefaultSignInExperience.mockResolvedValueOnce({
|
|
||||||
...mockSignInExperience,
|
|
||||||
signIn: {
|
|
||||||
methods: [
|
|
||||||
{
|
|
||||||
...mockSignInMethod,
|
|
||||||
identifier: SignInIdentifier.Sms,
|
|
||||||
password: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const response = await sessionRequest.post(`${signInRoute}/username`).send({
|
|
||||||
username: 'username',
|
|
||||||
password: 'password',
|
|
||||||
});
|
|
||||||
expect(response.statusCode).toEqual(422);
|
|
||||||
});
|
});
|
||||||
|
expect(response.statusCode).toEqual(200);
|
||||||
|
expect(response.body).toHaveProperty('redirectTo');
|
||||||
|
expect(interactionResult).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({ login: { accountId: 'id' } }),
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /session/register/password/username', () => {
|
describe('POST /session/register/password/username', () => {
|
||||||
|
|
|
@ -6,14 +6,20 @@ import { object, string } from 'zod';
|
||||||
|
|
||||||
import RequestError from '@/errors/RequestError';
|
import RequestError from '@/errors/RequestError';
|
||||||
import { assignInteractionResults } from '@/lib/session';
|
import { assignInteractionResults } from '@/lib/session';
|
||||||
import { verifyUserPassword, encryptUserPassword, generateUserId, insertUser } from '@/lib/user';
|
import { encryptUserPassword, generateUserId, insertUser } from '@/lib/user';
|
||||||
import koaGuard from '@/middleware/koa-guard';
|
import koaGuard from '@/middleware/koa-guard';
|
||||||
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
|
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
|
||||||
import { findUserByUsername, hasActiveUsers, hasUser, updateUserById } from '@/queries/user';
|
import {
|
||||||
|
findUserByEmail,
|
||||||
|
findUserByPhone,
|
||||||
|
findUserByUsername,
|
||||||
|
hasActiveUsers,
|
||||||
|
hasUser,
|
||||||
|
} from '@/queries/user';
|
||||||
import assertThat from '@/utils/assert-that';
|
import assertThat from '@/utils/assert-that';
|
||||||
|
|
||||||
import type { AnonymousRouter } from '../types';
|
import type { AnonymousRouter } from '../types';
|
||||||
import { getRoutePrefix } from './utils';
|
import { getRoutePrefix, signInWithPassword } from './utils';
|
||||||
|
|
||||||
export const registerRoute = getRoutePrefix('register', 'password');
|
export const registerRoute = getRoutePrefix('register', 'password');
|
||||||
export const signInRoute = getRoutePrefix('sign-in', 'password');
|
export const signInRoute = getRoutePrefix('sign-in', 'password');
|
||||||
|
@ -28,28 +34,61 @@ export default function passwordRoutes<T extends AnonymousRouter>(router: T, pro
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const signInExperience = await findDefaultSignInExperience();
|
|
||||||
assertThat(
|
|
||||||
signInExperience.signIn.methods.some(
|
|
||||||
({ identifier, password }) => identifier === SignInIdentifier.Username && password
|
|
||||||
),
|
|
||||||
new RequestError({
|
|
||||||
code: 'user.sign_in_method_not_enabled',
|
|
||||||
status: 422,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await provider.interactionDetails(ctx.req, ctx.res);
|
|
||||||
const { username, password } = ctx.guard.body;
|
const { username, password } = ctx.guard.body;
|
||||||
const type = 'SignInUsernamePassword';
|
const type = 'SignInUsernamePassword';
|
||||||
ctx.log(type, { username });
|
await signInWithPassword(ctx, provider, {
|
||||||
|
identifier: SignInIdentifier.Username,
|
||||||
|
password,
|
||||||
|
logType: type,
|
||||||
|
logPayload: { username },
|
||||||
|
findUser: async () => findUserByUsername(username),
|
||||||
|
});
|
||||||
|
|
||||||
const user = await findUserByUsername(username);
|
return next();
|
||||||
const { id } = await verifyUserPassword(user, password);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
ctx.log(type, { userId: id });
|
router.post(
|
||||||
await updateUserById(id, { lastSignInAt: Date.now() });
|
`${signInRoute}/email`,
|
||||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } }, true);
|
koaGuard({
|
||||||
|
body: object({
|
||||||
|
email: string().min(1),
|
||||||
|
password: string().min(1),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (ctx, next) => {
|
||||||
|
const { email, password } = ctx.guard.body;
|
||||||
|
const type = 'SignInEmailPassword';
|
||||||
|
await signInWithPassword(ctx, provider, {
|
||||||
|
identifier: SignInIdentifier.Email,
|
||||||
|
password,
|
||||||
|
logType: type,
|
||||||
|
logPayload: { email },
|
||||||
|
findUser: async () => findUserByEmail(email),
|
||||||
|
});
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
`${signInRoute}/sms`,
|
||||||
|
koaGuard({
|
||||||
|
body: object({
|
||||||
|
phone: string().min(1),
|
||||||
|
password: string().min(1),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (ctx, next) => {
|
||||||
|
const { phone, password } = ctx.guard.body;
|
||||||
|
const type = 'SignInSmsPassword';
|
||||||
|
await signInWithPassword(ctx, provider, {
|
||||||
|
identifier: SignInIdentifier.Sms,
|
||||||
|
password,
|
||||||
|
logType: type,
|
||||||
|
logPayload: { phone },
|
||||||
|
findUser: async () => findUserByPhone(phone),
|
||||||
|
});
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
184
packages/core/src/routes/session/utils.test.ts
Normal file
184
packages/core/src/routes/session/utils.test.ts
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import type { User } from '@logto/schemas';
|
||||||
|
import { UserRole, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
|
||||||
|
import { createMockContext } from '@shopify/jest-koa-mocks';
|
||||||
|
import type { Nullable } from '@silverhand/essentials';
|
||||||
|
import { Provider } from 'oidc-provider';
|
||||||
|
|
||||||
|
import { mockSignInExperience, mockSignInMethod, mockUser } from '@/__mocks__';
|
||||||
|
import RequestError from '@/errors/RequestError';
|
||||||
|
|
||||||
|
import { signInWithPassword } from './utils';
|
||||||
|
|
||||||
|
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||||
|
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||||
|
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||||
|
const hasActiveUsers = jest.fn(async () => true);
|
||||||
|
const findDefaultSignInExperience = jest.fn(async () => ({
|
||||||
|
...mockSignInExperience,
|
||||||
|
signUp: {
|
||||||
|
...mockSignInExperience.signUp,
|
||||||
|
identifier: SignUpIdentifier.Username,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/queries/user', () => ({
|
||||||
|
findUserById: async () => findUserById(),
|
||||||
|
findUserByIdentity: async () => ({ id: 'id', identities: {} }),
|
||||||
|
findUserByPhone: async () => ({ id: 'id' }),
|
||||||
|
findUserByEmail: async () => ({ id: 'id' }),
|
||||||
|
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||||
|
hasUser: async (username: string) => username === 'username1',
|
||||||
|
hasUserWithIdentity: async (connectorId: string, userId: string) =>
|
||||||
|
connectorId === 'connectorId' && userId === 'id',
|
||||||
|
hasUserWithPhone: async (phone: string) => phone === '13000000000',
|
||||||
|
hasUserWithEmail: async (email: string) => email === 'a@a.com',
|
||||||
|
hasActiveUsers: async () => hasActiveUsers(),
|
||||||
|
async findUserByUsername(username: string) {
|
||||||
|
const roleNames = username === 'admin' ? [UserRole.Admin] : [];
|
||||||
|
|
||||||
|
return { id: 'user1', username, roleNames };
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/queries/sign-in-experience', () => ({
|
||||||
|
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/lib/user', () => ({
|
||||||
|
async verifyUserPassword(user: Nullable<User>, password: string) {
|
||||||
|
if (!user) {
|
||||||
|
throw new RequestError('session.invalid_credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== 'password') {
|
||||||
|
throw new RequestError('session.invalid_credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
generateUserId: () => 'user1',
|
||||||
|
encryptUserPassword: (password: string) => ({
|
||||||
|
passwordEncrypted: password + '_user1',
|
||||||
|
passwordEncryptionMethod: 'Argon2i',
|
||||||
|
}),
|
||||||
|
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
|
||||||
|
insertUser: async (...args: unknown[]) => insertUser(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const grantSave = jest.fn(async () => 'finalGrantId');
|
||||||
|
const grantAddOIDCScope = jest.fn();
|
||||||
|
const grantAddResourceScope = jest.fn();
|
||||||
|
const interactionResult = jest.fn(async () => 'redirectTo');
|
||||||
|
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({}));
|
||||||
|
|
||||||
|
class Grant {
|
||||||
|
static async find(id: string) {
|
||||||
|
return id === 'exists' ? new Grant() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
save: typeof grantSave;
|
||||||
|
addOIDCScope: typeof grantAddOIDCScope;
|
||||||
|
addResourceScope: typeof grantAddResourceScope;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.save = grantSave;
|
||||||
|
this.addOIDCScope = grantAddOIDCScope;
|
||||||
|
this.addResourceScope = grantAddResourceScope;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createContext = () => ({
|
||||||
|
...createMockContext(),
|
||||||
|
addLogContext: jest.fn(),
|
||||||
|
log: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createProvider = () => new Provider('');
|
||||||
|
|
||||||
|
jest.mock('oidc-provider', () => ({
|
||||||
|
Provider: jest.fn(() => ({
|
||||||
|
Grant,
|
||||||
|
interactionDetails,
|
||||||
|
interactionResult,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
grantSave.mockClear();
|
||||||
|
interactionResult.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('signInWithPassword()', () => {
|
||||||
|
it('assign result', async () => {
|
||||||
|
interactionDetails.mockResolvedValueOnce({ params: {} });
|
||||||
|
await signInWithPassword(createContext(), createProvider(), {
|
||||||
|
identifier: SignInIdentifier.Username,
|
||||||
|
password: 'password',
|
||||||
|
findUser: jest.fn(async () => mockUser),
|
||||||
|
logType: 'SignInUsernamePassword',
|
||||||
|
logPayload: { username: 'username' },
|
||||||
|
});
|
||||||
|
expect(interactionResult).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({ login: { accountId: mockUser.id } }),
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw if user not found', async () => {
|
||||||
|
interactionDetails.mockResolvedValueOnce({ params: {} });
|
||||||
|
await expect(
|
||||||
|
signInWithPassword(createContext(), createProvider(), {
|
||||||
|
identifier: SignInIdentifier.Username,
|
||||||
|
password: 'password',
|
||||||
|
findUser: jest.fn(async () => null),
|
||||||
|
logType: 'SignInUsernamePassword',
|
||||||
|
logPayload: { username: 'username' },
|
||||||
|
})
|
||||||
|
).rejects.toThrowError(new RequestError('session.invalid_credentials'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw if user found but wrong password', async () => {
|
||||||
|
interactionDetails.mockResolvedValueOnce({ params: {} });
|
||||||
|
await expect(
|
||||||
|
signInWithPassword(createContext(), createProvider(), {
|
||||||
|
identifier: SignInIdentifier.Username,
|
||||||
|
password: '_password',
|
||||||
|
findUser: jest.fn(async () => mockUser),
|
||||||
|
logType: 'SignInUsernamePassword',
|
||||||
|
logPayload: { username: 'username' },
|
||||||
|
})
|
||||||
|
).rejects.toThrowError(new RequestError('session.invalid_credentials'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw if sign in method is not enabled', async () => {
|
||||||
|
findDefaultSignInExperience.mockResolvedValueOnce({
|
||||||
|
...mockSignInExperience,
|
||||||
|
signIn: {
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
...mockSignInMethod,
|
||||||
|
identifier: SignInIdentifier.Sms,
|
||||||
|
password: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
interactionDetails.mockResolvedValueOnce({ params: {} });
|
||||||
|
await expect(
|
||||||
|
signInWithPassword(createContext(), createProvider(), {
|
||||||
|
identifier: SignInIdentifier.Username,
|
||||||
|
password: 'password',
|
||||||
|
findUser: jest.fn(async () => mockUser),
|
||||||
|
logType: 'SignInUsernamePassword',
|
||||||
|
logPayload: { username: 'username' },
|
||||||
|
})
|
||||||
|
).rejects.toThrowError(
|
||||||
|
new RequestError({
|
||||||
|
code: 'user.sign_in_method_not_enabled',
|
||||||
|
status: 422,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,6 @@
|
||||||
import type { LogType, PasscodeType } from '@logto/schemas';
|
import type { LogPayload, LogType, PasscodeType, SignInIdentifier, User } from '@logto/schemas';
|
||||||
import { logTypeGuard } from '@logto/schemas';
|
import { logTypeGuard } from '@logto/schemas';
|
||||||
import type { Truthy } from '@silverhand/essentials';
|
import type { Nullable, Truthy } from '@silverhand/essentials';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import type { Provider } from 'oidc-provider';
|
import type { Provider } from 'oidc-provider';
|
||||||
|
@ -8,6 +8,11 @@ import type { ZodType } from 'zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import RequestError from '@/errors/RequestError';
|
import RequestError from '@/errors/RequestError';
|
||||||
|
import { assignInteractionResults } from '@/lib/session';
|
||||||
|
import { verifyUserPassword } from '@/lib/user';
|
||||||
|
import type { LogContext } from '@/middleware/koa-log';
|
||||||
|
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
|
||||||
|
import { updateUserById } from '@/queries/user';
|
||||||
import assertThat from '@/utils/assert-that';
|
import assertThat from '@/utils/assert-that';
|
||||||
|
|
||||||
import { verificationTimeout } from './consts';
|
import { verificationTimeout } from './consts';
|
||||||
|
@ -94,3 +99,38 @@ export const clearVerificationResult = async (ctx: Context, provider: Provider)
|
||||||
await provider.interactionResult(ctx.req, ctx.res, rest);
|
await provider.interactionResult(ctx.req, ctx.res, rest);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SignInWithPasswordParameter = {
|
||||||
|
identifier: SignInIdentifier;
|
||||||
|
password: string;
|
||||||
|
logType: LogType;
|
||||||
|
logPayload: LogPayload;
|
||||||
|
findUser: () => Promise<Nullable<User>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const signInWithPassword = async (
|
||||||
|
ctx: Context & LogContext,
|
||||||
|
provider: Provider,
|
||||||
|
{ identifier, findUser, password, logType, logPayload }: SignInWithPasswordParameter
|
||||||
|
) => {
|
||||||
|
const signInExperience = await findDefaultSignInExperience();
|
||||||
|
assertThat(
|
||||||
|
signInExperience.signIn.methods.some(
|
||||||
|
(method) => method.password && method.identifier === identifier
|
||||||
|
),
|
||||||
|
new RequestError({
|
||||||
|
code: 'user.sign_in_method_not_enabled',
|
||||||
|
status: 422,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await provider.interactionDetails(ctx.req, ctx.res);
|
||||||
|
ctx.log(logType, logPayload);
|
||||||
|
|
||||||
|
const user = await findUser();
|
||||||
|
const { id } = await verifyUserPassword(user, password);
|
||||||
|
|
||||||
|
ctx.log(logType, { userId: id });
|
||||||
|
await updateUserById(id, { lastSignInAt: Date.now() });
|
||||||
|
await assignInteractionResults(ctx, provider, { login: { accountId: id } }, true);
|
||||||
|
};
|
||||||
|
|
|
@ -72,8 +72,12 @@ export const setUpConnector = async (connectorId: string, config: Record<string,
|
||||||
assert(connector.enabled, new Error('Connector Setup Failed'));
|
assert(connector.enabled, new Error('Connector Setup Failed'));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setSignUpIdentifier = async (identifier: SignUpIdentifier) => {
|
export const setSignUpIdentifier = async (
|
||||||
await updateSignInExperience({ signUp: { identifier, password: true, verify: true } });
|
identifier: SignUpIdentifier,
|
||||||
|
password = true,
|
||||||
|
verify = true
|
||||||
|
) => {
|
||||||
|
await updateSignInExperience({ signUp: { identifier, password, verify } });
|
||||||
};
|
};
|
||||||
|
|
||||||
type PasscodeRecord = {
|
type PasscodeRecord = {
|
||||||
|
|
|
@ -47,7 +47,7 @@ describe('username and password flow', () => {
|
||||||
describe('email passwordless flow', () => {
|
describe('email passwordless flow', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setUpConnector(mockEmailConnectorId, mockEmailConnectorConfig);
|
await setUpConnector(mockEmailConnectorId, mockEmailConnectorConfig);
|
||||||
await setSignUpIdentifier(SignUpIdentifier.Email);
|
await setSignUpIdentifier(SignUpIdentifier.Email, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Since we can not create a email register user throw admin. Have to run the register then sign-in concurrently.
|
// Since we can not create a email register user throw admin. Have to run the register then sign-in concurrently.
|
||||||
|
@ -121,7 +121,7 @@ describe('email passwordless flow', () => {
|
||||||
describe('sms passwordless flow', () => {
|
describe('sms passwordless flow', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setUpConnector(mockSmsConnectorId, mockSmsConnectorConfig);
|
await setUpConnector(mockSmsConnectorId, mockSmsConnectorConfig);
|
||||||
await setSignUpIdentifier(SignUpIdentifier.Sms);
|
await setSignUpIdentifier(SignUpIdentifier.Sms, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Since we can not create a sms register user throw admin. Have to run the register then sign-in concurrently.
|
// Since we can not create a sms register user throw admin. Have to run the register then sign-in concurrently.
|
||||||
|
|
|
@ -79,6 +79,20 @@ const signInUsernamePasswordLogPayloadGuard = arbitraryLogPayloadGuard.and(
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const signInEmailPasswordLogPayloadGuard = arbitraryLogPayloadGuard.and(
|
||||||
|
z.object({
|
||||||
|
userId: z.string().optional(),
|
||||||
|
email: z.string().optional(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const signInSmsPasswordLogPayloadGuard = arbitraryLogPayloadGuard.and(
|
||||||
|
z.object({
|
||||||
|
userId: z.string().optional(),
|
||||||
|
sms: z.string().optional(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const signInEmailSendPasscodeLogPayloadGuard = arbitraryLogPayloadGuard.and(
|
const signInEmailSendPasscodeLogPayloadGuard = arbitraryLogPayloadGuard.and(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().optional(),
|
email: z.string().optional(),
|
||||||
|
@ -197,6 +211,8 @@ const logPayloadsGuard = z.object({
|
||||||
RegisterSocialBind: registerSocialBindLogPayloadGuard,
|
RegisterSocialBind: registerSocialBindLogPayloadGuard,
|
||||||
RegisterSocial: registerSocialLogPayloadGuard,
|
RegisterSocial: registerSocialLogPayloadGuard,
|
||||||
SignInUsernamePassword: signInUsernamePasswordLogPayloadGuard,
|
SignInUsernamePassword: signInUsernamePasswordLogPayloadGuard,
|
||||||
|
SignInEmailPassword: signInEmailPasswordLogPayloadGuard,
|
||||||
|
SignInSmsPassword: signInSmsPasswordLogPayloadGuard,
|
||||||
SignInEmailSendPasscode: signInEmailSendPasscodeLogPayloadGuard,
|
SignInEmailSendPasscode: signInEmailSendPasscodeLogPayloadGuard,
|
||||||
SignInEmail: signInEmailLogPayloadGuard,
|
SignInEmail: signInEmailLogPayloadGuard,
|
||||||
SignInSmsSendPasscode: signInSmsSendPasscodeLogPayloadGuard,
|
SignInSmsSendPasscode: signInSmsSendPasscodeLogPayloadGuard,
|
||||||
|
|
Loading…
Add table
Reference in a new issue