0
Fork 0
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:
Wang Sijie 2022-01-28 11:04:38 +08:00 committed by GitHub
parent 6a6ae9d7d5
commit ea2bfd6d45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 170 additions and 0 deletions

View 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']);
});
});

View 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,
});
};

View 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();
}
};