mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): createPasscode (#199)
* feat: createPasscode * test: passcode UT * fix: consume * fix: multiple unconsumed passcodes * fix: pr * fix: lint
This commit is contained in:
parent
6a6ae9d7d5
commit
ea2bfd6d45
3 changed files with 170 additions and 0 deletions
80
packages/core/src/lib/passcode.test.ts
Normal file
80
packages/core/src/lib/passcode.test.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { PasscodeType } from '@logto/schemas';
|
||||
|
||||
import {
|
||||
deletePasscodesByIds,
|
||||
findUnconsumedPasscodesBySessionIdAndType,
|
||||
insertPasscode,
|
||||
} from '@/queries/passcode';
|
||||
|
||||
import { createPasscode, passcodeLength } from './passcode';
|
||||
|
||||
jest.mock('@/queries/passcode');
|
||||
|
||||
const mockedFindUnconsumedPasscodesBySessionIdAndType =
|
||||
findUnconsumedPasscodesBySessionIdAndType as jest.MockedFunction<
|
||||
typeof findUnconsumedPasscodesBySessionIdAndType
|
||||
>;
|
||||
const mockedDeletePasscodesByIds = deletePasscodesByIds as jest.MockedFunction<
|
||||
typeof deletePasscodesByIds
|
||||
>;
|
||||
const mockedInsertPasscode = insertPasscode as jest.MockedFunction<typeof insertPasscode>;
|
||||
|
||||
beforeAll(() => {
|
||||
mockedFindUnconsumedPasscodesBySessionIdAndType.mockResolvedValue([]);
|
||||
mockedInsertPasscode.mockImplementation(async (data) => ({
|
||||
...data,
|
||||
createdAt: Date.now(),
|
||||
phone: data.phone ?? null,
|
||||
email: data.email ?? null,
|
||||
consumed: data.consumed ?? false,
|
||||
tryCount: data.tryCount ?? 0,
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockedFindUnconsumedPasscodesBySessionIdAndType.mockClear();
|
||||
mockedDeletePasscodesByIds.mockClear();
|
||||
mockedInsertPasscode.mockClear();
|
||||
});
|
||||
|
||||
describe('createPasscode', () => {
|
||||
it('should generate `passcodeLength` digits code for phone and insert to database', async () => {
|
||||
const phone = '13000000000';
|
||||
const passcode = await createPasscode('sessionId', PasscodeType.SignIn, {
|
||||
phone,
|
||||
});
|
||||
expect(new RegExp(`^\\d{${passcodeLength}}$`).test(passcode.code)).toBeTruthy();
|
||||
expect(passcode.phone).toEqual(phone);
|
||||
});
|
||||
|
||||
it('should generate `passcodeLength` digits code for email and insert to database', async () => {
|
||||
const email = 'jony@example.com';
|
||||
const passcode = await createPasscode('sessionId', PasscodeType.SignIn, {
|
||||
email,
|
||||
});
|
||||
expect(new RegExp(`^\\d{${passcodeLength}}$`).test(passcode.code)).toBeTruthy();
|
||||
expect(passcode.email).toEqual(email);
|
||||
});
|
||||
|
||||
it('should disable existing passcode', async () => {
|
||||
const email = 'jony@example.com';
|
||||
const sessionId = 'sessonId';
|
||||
mockedFindUnconsumedPasscodesBySessionIdAndType.mockResolvedValue([
|
||||
{
|
||||
id: 'id',
|
||||
sessionId,
|
||||
code: '1234',
|
||||
type: PasscodeType.SignIn,
|
||||
createdAt: Date.now(),
|
||||
phone: '',
|
||||
email,
|
||||
consumed: false,
|
||||
tryCount: 0,
|
||||
},
|
||||
]);
|
||||
await createPasscode(sessionId, PasscodeType.SignIn, {
|
||||
email,
|
||||
});
|
||||
expect(mockedDeletePasscodesByIds).toHaveBeenCalledWith(['id']);
|
||||
});
|
||||
});
|
32
packages/core/src/lib/passcode.ts
Normal file
32
packages/core/src/lib/passcode.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { PasscodeType } from '@logto/schemas';
|
||||
import { customAlphabet, nanoid } from 'nanoid';
|
||||
|
||||
import {
|
||||
deletePasscodesByIds,
|
||||
findUnconsumedPasscodesBySessionIdAndType,
|
||||
insertPasscode,
|
||||
} from '@/queries/passcode';
|
||||
|
||||
export const passcodeLength = 6;
|
||||
const randomCode = customAlphabet('1234567890', passcodeLength);
|
||||
|
||||
export const createPasscode = async (
|
||||
sessionId: string,
|
||||
type: PasscodeType,
|
||||
payload: { phone: string } | { email: string }
|
||||
) => {
|
||||
// Disable existing passcodes.
|
||||
const passcodes = await findUnconsumedPasscodesBySessionIdAndType(sessionId, type);
|
||||
|
||||
if (passcodes.length > 0) {
|
||||
await deletePasscodesByIds(passcodes.map(({ id }) => id));
|
||||
}
|
||||
|
||||
return insertPasscode({
|
||||
id: nanoid(),
|
||||
sessionId,
|
||||
type,
|
||||
code: randomCode(),
|
||||
...payload,
|
||||
});
|
||||
};
|
58
packages/core/src/queries/passcode.ts
Normal file
58
packages/core/src/queries/passcode.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { PasscodeType, Passcode, Passcodes, CreatePasscode } from '@logto/schemas';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
import { buildInsertInto } from '@/database/insert-into';
|
||||
import pool from '@/database/pool';
|
||||
import { buildUpdateWhere } from '@/database/update-where';
|
||||
import { convertToIdentifiers } from '@/database/utils';
|
||||
import { DeletionError } from '@/errors/SlonikError';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(Passcodes);
|
||||
|
||||
export const findUnconsumedPasscodeBySessionIdAndType = async (
|
||||
sessionId: string,
|
||||
type: PasscodeType
|
||||
) =>
|
||||
pool.one<Passcode>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.sessionId}=${sessionId} and ${fields.type}=${type} and ${fields.consumed} = false
|
||||
`);
|
||||
|
||||
export const findUnconsumedPasscodesBySessionIdAndType = async (
|
||||
sessionId: string,
|
||||
type: PasscodeType
|
||||
) =>
|
||||
pool.many<Passcode>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.sessionId}=${sessionId} and ${fields.type}=${type} and ${fields.consumed} = false
|
||||
`);
|
||||
|
||||
export const insertPasscode = buildInsertInto<CreatePasscode, Passcode>(pool, Passcodes, {
|
||||
returning: true,
|
||||
});
|
||||
|
||||
export const updatePasscode = buildUpdateWhere<CreatePasscode, Passcode>(pool, Passcodes, true);
|
||||
|
||||
export const deletePasscodeById = async (id: string) => {
|
||||
const { rowCount } = await pool.query(sql`
|
||||
delete from ${table}
|
||||
where id=${id}
|
||||
`);
|
||||
|
||||
if (rowCount < 1) {
|
||||
throw new DeletionError();
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePasscodesByIds = async (ids: string[]) => {
|
||||
const { rowCount } = await pool.query(sql`
|
||||
delete from ${table}
|
||||
where id in (${ids.join(',')})
|
||||
`);
|
||||
|
||||
if (rowCount < 1) {
|
||||
throw new DeletionError();
|
||||
}
|
||||
};
|
Loading…
Reference in a new issue