Подтверждение действий пользователя
Принцип работы
В рамках задачи STUDWORK-4294 был переопределен принцип работы подтверждений пользователя, делая их унифицированными.
- Фронтенд делает запрос к апи
- Если пользователю требуется подтвердить действие, апи отдает ошибку с кодом
449(AxiosError<ConfirmOptions>)
Подробно об ответах апи (`registry/repositories/user/confirm.ts`)
import { useRegistryWrapper, type RegistryKey } from '#sf/modules/registry';
import type { UserConfirmationType } from '#kb/consts/user-confirmation-type';
import type { Transport } from '#kb/consts/transports';
import type { Implements } from '#sf/types/utils';
export type AvailableTransport = Exclude<Transport, Transport.FLASHCALL>;
export interface TransportOptions {
/**
* id транспорта
*/
transport: AvailableTransport;
/**
* Параметры ввода кода
*/
attempts: {
/**
* Количество оставшихся попыток ввода кода
*/
remaining: number;
/**
* Дата в ISO следующей попытки
*/
next: string;
};
/**
* Дополнительная информация
*/
data?: unknown;
/**
* Правильный код подтверждения (only dev)
*/
code?: string;
}
export interface ResendTransportOptions extends TransportOptions {
/**
* Параметры переотправки кода
*/
codes: {
/**
* Количество оставшихся попыток переотправок
*/
remaining: number;
/**
* Дата в ISO следующей попытки
*/
next: string;
};
/**
* Дата последнего отправленного кода в ISO
*/
lastCodeAt: string | null;
}
export interface PhoneTransportOptions extends ResendTransportOptions {
data: {
/**
* Номер телефона получателя
*/
phone: string;
};
}
export interface EmailTransportOptions extends ResendTransportOptions {
data: {
/**
* Email получателя
*/
email: string;
};
}
export interface TotpTransportOptions extends TransportOptions {}
type TransportMap = {
[Key in AvailableTransport]: TransportOptions;
};
/**
* Список возможных транспортов с сопоставленными опциями
*/
export interface AvailableTransports {
[Transport.EMAIL]: EmailTransportOptions;
[Transport.SMS]: PhoneTransportOptions;
[Transport.TOTP]: TotpTransportOptions;
[Transport.CASCADE_PHONE]: PhoneTransportOptions;
}
type A = Implements<TransportMap, AvailableTransports>;
/**
* Хелпер для возвращания транспортов по его опциям
*/
export type TransportWithOptions<T extends TransportOptions> = {
[K in keyof AvailableTransports]: AvailableTransports[K] extends T
? K
: never;
}[keyof AvailableTransports];
/**
* Настройки подтверждения конкретного действия
*/
export interface ConfirmOptions {
/**
* Приоритетный/первый `transport` для отображения юзеру
*
* Приходит с бекенда т.к. для части подтверждений бек должен сперва сделать действие, например отправить смс
*/
activeTransport: AvailableTransport;
/**
* Название действия для подтверждения (используется лишь для обратной передачи бекенду)
*/
type: UserConfirmationType;
/**
* Идентификатор подтверждения
*/
token: string;
/**
* Возможные варианты подтверждения
*/
transports: Array<AvailableTransports[keyof AvailableTransports]>;
}
export interface ConfirmResponse {
confirmation: ConfirmOptions;
}
interface GetContactsParams {
type: UserConfirmationType;
}
export interface IUserConfirmRepo {
/**
* Попытка ввода кода подтверждения
*
* @throws {AxiosError\<ConfirmResponse>} 449 - Кончились попытки ввода кода
*/
confirm: <T>(token: string, transport: Transport, code: string) => Promise<T>;
/**
* Переотправка (или смена транспорта)
*
* @throws {AxiosError\<ConfirmResponse>} 449 - Кончились попытки отправки
*/
resend: (token: string, transport: Transport) => Promise<ConfirmResponse>;
get: (params: Partial<GetContactsParams>) => Promise<{
confirmations: ConfirmOptions[];
}>;
}
export const userConfirmRepoRegistryKey =
'user-confirm-repo-registry-key' as unknown as RegistryKey<IUserConfirmRepo>;
export default useRegistryWrapper(userConfirmRepoRegistryKey);
- Фронт отображает(или через форму, или модалкой) первый способ (
activeTransport) пользователю и если есть альтернативы, дает возможность выбора иного способа- Юзер может выбрать иной способ, для этого фронт делает запрос
resendс параметром нового транспорта
- Юзер может выбрать иной способ, для этого фронт делает запрос
- После успешного подтверждения, бекенд отдает изначальный ответ из шага 1
- Бекенд так же может вернуть
449но с другимtoken, что значит, что необходимо второе подтверждение и мы возвращаемся к шагу 2
- Бекенд так же может вернуть
Добавление транспортов
- Добавить новый транспорт в
kb(shared/data/transports.yml) - Добавить в
#sf/components/user/confirm-2fa/config.tsвtransportConfigsконфиг этого транспорта с указанием компонента (подробнее далее) (если не добавить новый транспорт,typescriptбудет выдавать ошибку) - Написать компонент для транспорта
Компонент транспорта
Каждый компонент может эмитить 2 события, success в случае успешного подтверждения, exceed если кончились попытки ввода и nextConfirmation если изменилось подтверждение (сменился идентификатор)
бОльшую часть логики берут на себя composable useConfirmApi(отвечает за апи запросы), useOtpInput(за работу с инпутом ввода кодов и его состояния), useResend(если код можно переотправить, то за текст и состояние кнопки переотправки)
Наиболее типичным (с переотправкой) компонентом является email-confirmation
Логика работы экранов транспортов
Ядром фронта для работы с подтверждениями является файл #sf/components/user/confirm2-fa/composables/change-transport.ts
Для смены транспортов необходимо использовать его, т.к. под капотом помимо смены компонента, он же производит запрос кода, если этого не было сделано