feat(TEAMMSBMOB-17106): Перенесена библиотека @eco/crypto

This commit is contained in:
Онуфрийчук Егор
2025-08-05 12:56:27 +03:00
parent 1075b8e2ea
commit 37b49e0d2a
318 changed files with 15275 additions and 410 deletions
+1
View File
@@ -77,4 +77,5 @@ const config: IWebpackAppConfig = {
export default config;
```
1. Для того, чтобы поднять приложение интернет-банка в режиме 'production', необходимо запустить команду npm run start:prod. В последующих итерациях нужно будет перед запуском скрипта удалить ранее созданный докер контейнер и образ.
+1658 -306
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -1,17 +1,24 @@
{
"name": "msb-platform-monorepo",
"version": "1.0.0-beta.7",
"version": "1.0.0-beta.8",
"files": ["msb-host", "msb-main-page", "msb-deposits", "msb-payments", "msb-statements-and-inquiries", "msb-accounts"],
"workspaces": ["packages/*", "services/*"],
"private": false,
"scripts": {
"start": "lerna run start --stream",
"start:msw": "lerna run start --stream",
"start:sandbox": "lerna run start:sandbox --stream",
"start:statements": "lerna run start --scope=msb-host --scope=msb-statements-and-inquiries --stream",
"start:sandbox:statements": "lerna run start:sandbox --scope=msb-host --stream & lerna run start --scope=msb-statements-and-inquiries --stream",
"start:main-page": "lerna run start --scope=msb-host --scope=msb-main-page --stream",
"start:sandbox:main-page": "lerna run start:sandbox --scope=msb-host --stream & lerna run start --scope=msb-main-page --stream",
"start:deposits": "lerna run start --scope=msb-host --scope=msb-deposits --stream",
"start:sandbox:deposits": "lerna run start:sandbox --scope=msb-host --stream & lerna run start --scope=msb-deposits --stream",
"start:payments": "lerna run start --scope=msb-host --scope=msb-payments --stream",
"start:sandbox:payments": "lerna run start:sandbox --scope=msb-host --stream & lerna run start --scope=msb-payments --stream",
"start:accounts": "lerna run start --scope=msb-host --scope=msb-accounts --stream",
"start:sandbox:accounts": "lerna run start:sandbox --stream & lerna run start --scope=msb-host --scope=msb-accounts --stream",
"start:service": "lerna run start --scope=msb-host --stream",
"start:sandbox:service": "lerna run start:sandbox --scope=msb-host --stream",
"build": "lerna run build --scope=msb-* --stream --concurrency=1",
"build:webpack-config": "lerna run build --scope=@msb/mf-builder --stream",
"start:prod": "npm run build && docker-compose up",
+165
View File
@@ -0,0 +1,165 @@
# Библиотека для работы с подписью данных @msb/crypto
1. Состоит из 4 модулей:
- core - ядро библиотеки для работы с подписью данных
- install - модуль для проверки и установки криптомодуля
- sign - модуль для подписи данных
- verify - модуль для проверки подписи данных
1. Установить зависимости:
```bash
npm i @msb/crypto
```
1. Инструкция для установки криптомодуля и сертификатов (https://confluence.gboteam.ru/pages/viewpage.action?pageId=121085377)
1. Пример использования подписания документов:
```ts
/* eslint-disable @typescript-eslint/no-confusing-void-expression */
import type { FC } from 'react';
import React, { useState } from 'react';
import { Button } from '@fractal-ui/core';
import type { ISignModalProps, ISignModalApi, ISignRequestData, IBaseEntity } from '@msb/crypto';
import { openSignModal } from '@msb/crypto';
const service = {
sign: (docs: ISignRequestData[]) => docs.map((doc, index) => ({ id: doc.id ?? `ID ${index}` })),
getSignData: async () => {
const response = {
data: {
signDocuments: [
{
documentId: 'db20e321-7ed1-41a8-865b-b6b7ef6097a1',
clientId: 'fb188766-6eff-4122-897e-0c24364c4ea2',
signDataList: [
{
id: 'db20e321-7ed1-41a8-865b-b6b7ef6097a4',
label: 'Письмо в Банк №28 от 08.08.2025',
dssLabel: 'Письмо в Банк №28 от 08.08.2025',
digest:
'eyJpZCI6ImRiMjBlMzIxLTdlZDEtNDFhOC04NjViLWI2YjdlZjYwOTdhNCIsImRhdGUiOiIyMDI1LTA4LTA4IiwibnVtYmVyIjoyOCwiYmFua0NsaWVudEluZm8ubmFtZSI6ItCd0JrQniBcItCU0L7RgNGC0LXRhS3Qv9GA0LjRhtC10L9cIiIsImJhbmtDbGllbnRJbmZvLmludGVybmF0aW9uYWxOYW1lIjoiTktPIFwiRG9ydGVraC1wcml0c2VwXCIiLCJiYW5rQ2xpZW50SW5mby5pbm5LaW8iOiI3NzQ1MDMyMzYwIiwiYmFua0NsaWVudEluZm8ub2dybk9ncmlwIjoiNTg1OTQyOTM1MDY4NyIsInR5cGVOYW1lIjoidGVzdDIiLCJhdHRhY2htZW50cy5maWxlTmFtZSI6WyJ0ZXN0LnR4dCJdLCJhdHRhY2htZW50cy5maWxlSGFzaCI6WyIxMmE1MDgzODE5MWI1NTA0ZjFlNWYyZmQwNzg3MTRjZjZiNTkyYjlkMjlhZjk5ZDBiMTBkOGQwMjg4MWMzODU3Il0sImF0dGFjaG1lbnRzLmZpbGVTaXplIjpbNF0sImJyYW5jaEluZm8uZmlsaWFsTmFtZSI6ItCkLdCbINCR0JDQndCa0JAg0JPQn9CRICjQkNCeKSBcItCf0KDQmNCS0J7Qm9CW0KHQmtCY0JlcIiIsImJyYW5jaEluZm8uYmljIjoiMDQyMjAyNzY0IiwiYnJhbmNoSW5mby5jb2RlIjoiMDEwIn0=',
type: 'DOCUMENT',
signDataViewList: [
{
label: 'Основная информация',
signFieldsViewList: [
{
label: 'Тип документа',
value: 'test2',
},
],
},
{
label: 'Отправитель',
signFieldsViewList: [
{
label: 'Организация',
value: 'НКО "Дортех-прицеп"',
},
{
label: 'Международное наименование организации',
value: 'NKO "Dortekh-pritsep"',
},
{
label: 'ИНН',
value: '7745032360',
},
{
label: 'ОГРН',
value: '5859429350687',
},
],
},
{
label: 'Получатель',
signFieldsViewList: [
{
label: 'Филиал',
value: '010 Ф-Л БАНКА ГПБ (АО) "ПРИВОЛЖСКИЙ"',
},
{
label: 'БИК',
value: '042202764',
},
],
},
],
},
{
id: '526e7ed4-019b-489e-8f67-11c540e7179e',
label: 'Вложение №1',
dssLabel: 'Вложение №1',
type: 'ATTACHMENT',
signDataViewList: [
{
label: 'Описание вложения',
signFieldsViewList: [
{
label: 'Наименование файла',
value: 'test.txt',
},
{
label: 'Хешсумма',
value: '12a50838191b5504f1e5f2fd078714cf6b592b9d29af99d0b10d8d02881c3857',
},
{
label: 'Размер',
value: '4 б',
},
],
},
],
},
],
},
],
},
};
if (response?.data?.signDocuments) {
return { signDocuments: (response.data?.signDocuments ?? []).map(signDocument => ({ ...signDocument, clientId: '' })) };
}
return Promise.reject({ error: '' });
},
send: (sendData: IBaseEntity[]) => Promise.resolve(sendData.map((doc, index) => doc.id ?? `ID ${index}`)),
} as unknown as ISignModalApi;
const SignDataComponent: FC<ISignModalProps['modalParams'] & Pick<ISignModalProps, 'signData' | 'type'>> = ({
signData,
type,
...props
}) => {
const handleClick = () => {
try {
openSignModal({
api: service,
signData: {
documentsRegistry: [
{
documents: [{ id: 'db20e321-7ed1-41a8-865b-b6b7ef6097a4' }],
clientId: 'fb188766-6eff-4122-897e-0c24364c4ea2',
clientName: '',
},
],
},
type,
modalParams: props,
});
} catch (e: unknown) {
console.error(JSON.stringify(e));
}
};
return (
<Button dataAction="open-installer" onClick={handleClick}>
Открыть
</Button>
);
};
export { SignDataComponent };
```
1. Пример использования @eco/crypto в стриме @eco/dsf (https://bitbucket.gboteam.ru/projects/ECO_FE/repos/eco-dsf/browse)
@@ -0,0 +1,21 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { LOCALE_NAME } from '../../stream-constants';
import { isLinux } from '../../utils/is-linux';
import { DownloadCryptoText } from '../download-crypto-linux';
import { WrappedText } from '../wrapped-text';
/**
* Отображение информации о необходимости установить КМ.
*/
export const CheckCryptoView: React.FC = () => {
const { t } = useTranslation(LOCALE_NAME);
return (
<WrappedText color={'text.secondary'}>
{isLinux() ? <DownloadCryptoText /> : t('sign.modal.cryptoInstallerModal.txt.NeedToInstallCryptomodulForRegCert')}
</WrappedText>
);
};
CheckCryptoView.displayName = 'CheckCryptoView';
@@ -0,0 +1,20 @@
import type { FC } from 'react';
import { Link } from '@fractal-ui/core';
import { useAppContext } from '@msb/shared';
import { SETTING_CODE } from '../interfaces';
/**
* Компонент ссылка на диагностику. Отображается при получении ошибки от КМ.
*/
export const DiagnosticLink: FC = ({ children }) => {
const { systemConfigs } = useAppContext();
const helpSettings = systemConfigs.systemPublicConfig.get(SETTING_CODE.HELP_SETTINGS) as Record<string, any>;
return helpSettings ? (
<Link href={helpSettings.startDiagnosticsUrl} rel="noopener noreferrer" target="_blank">
{children}
</Link>
) : null;
};
DiagnosticLink.displayName = 'DiagnosticLink';
@@ -0,0 +1,26 @@
import React from 'react';
import { Link } from '@fractal-ui/core';
import { useAppContext } from '@msb/shared';
import { Trans, useTranslation } from 'react-i18next';
import { SETTING_CODE } from '../../interfaces';
import { LOCALE_NAME } from '../../stream-constants';
/**
* Текст МО скачивания КМ для семейства Linux.
*/
export const DownloadCryptoText: React.FC = () => {
const { t } = useTranslation(LOCALE_NAME);
const { systemConfigs } = useAppContext();
const config = systemConfigs.systemClientConfig?.get(SETTING_CODE.SETTINGS) as Record<string, any>;
const cmLinuxInstructionUrl = config?.cmLinuxInstructionUrl ?? '';
const TextLink = <Link href={cmLinuxInstructionUrl} rel="noreferrer" target="_blank" />;
return <Trans components={[TextLink]} i18nKey={'sign.modal.cryptoInstallerModal.txt.installLinux'} t={t} />;
};
DownloadCryptoText.displayName = 'DownloadCryptoText';
+8
View File
@@ -0,0 +1,8 @@
import styled from '@emotion/styled';
import { Wrapper } from '@fractal-ui/styling';
import styledCss from '@styled-system/css';
/**
* Flex-контейнер.
*/
export const StyledFlexbox = styled(Wrapper)(styledCss({ display: 'flex' }));
@@ -0,0 +1,10 @@
import React from 'react';
import { Link } from '@fractal-ui/core';
/**
* Компонент-ссылка на базу знаний.
* Отображается при получении ошибок от КМ.
*/
export const HowToLink: React.FC<{ hashLink: string }> = ({ hashLink, children }) => <Link href={`/help#${hashLink}`}>{children}</Link>;
HowToLink.displayName = 'HowToLink';
+6
View File
@@ -0,0 +1,6 @@
export * from './modal-footer';
export * from './flex';
export * from './wrapped-text';
export * from './diagnostic-link';
export * from './how-to-link';
export * from './check';
@@ -0,0 +1,45 @@
import type { Align } from './types';
/**
* Типы кнопок МО.
*/
export enum BUTTON_TYPE {
/**
* Кнопка.
*/
BUTTON = 'button',
/**
* Ссылка.
*/
LINK = 'link',
/**
* Кнопка с выпадающем меню.
*/
DROPDOWN = 'dropdown',
/**
* Кнопка с тултипом.
*/
WITH_TOOLTIP = 'withTooltip',
}
/**
* Выравнивание кнопок.
*/
export const JUSTIFY: Record<Align, string> = {
/**
* По центру.
*/
center: 'center',
/**
* Слева.
*/
left: 'flex-start',
/**
* Справа.
*/
right: 'flex-end',
/**
* По бокам.
*/
justify: 'space-between',
};
@@ -0,0 +1,2 @@
export * from './modal-footer';
export type { ModalButtonProps, ActionsProps } from './types';
@@ -0,0 +1,63 @@
import React, { useRef } from 'react';
import { ContextMenuBase } from '@fractal-ui/composites';
import { Button, ButtonLink } from '@fractal-ui/core';
import { Tooltip } from '@fractal-ui/overlays';
import { BUTTON_TYPE } from './constants';
import { StyledActionWrapper } from './styled';
import type { ActionsProps, ModalButtonProps } from './types';
/**
* Компонент экшенов для модального окна.
*/
export const ModalFooter: React.FC<ActionsProps> = ({ actions = [], actionsDirection = 'horizontal', onClose, align = 'right' }) => {
const handleButtonClick = (e: React.MouseEvent, handler: React.MouseEventHandler | undefined, preventClose?: boolean) => {
handler?.(e);
!preventClose && onClose?.();
};
const buttonRef = useRef<HTMLButtonElement>(null);
return (
<StyledActionWrapper actionsDirection={actionsDirection} align={align}>
{actions.map((action: ModalButtonProps) => {
if (!action) return null;
const key = `${action.text}-${action.dataAction}`;
return (
<>
{action.actionBtnType === BUTTON_TYPE.BUTTON && (
<Button key={key} {...action} onClick={e => handleButtonClick(e, action.onClick, action?.preventClose)}>
{action.text}
</Button>
)}
{action.actionBtnType === BUTTON_TYPE.LINK && (
<ButtonLink key={key} {...action} role="link" onClick={e => handleButtonClick(e, action.onClick, action?.preventClose)}>
{action.text}
</ButtonLink>
)}
{action.actionBtnType === BUTTON_TYPE.DROPDOWN && (
<ContextMenuBase<HTMLButtonElement> key={key} disableAutowidth={false} items={action.items}>
{({ ref, toggleOpen }) => (
<Button ref={ref} {...action} onClick={toggleOpen}>
{action.text}
</Button>
)}
</ContextMenuBase>
)}
{action.actionBtnType === BUTTON_TYPE.WITH_TOOLTIP && (
<>
<Button key={key} ref={buttonRef} {...action} onClick={e => handleButtonClick(e, action.onClick, action?.preventClose)}>
{action.text}
</Button>
<Tooltip anchorEl={buttonRef} dataName="certificate_info" title={action.tooltip} />
</>
)}
</>
);
})}
</StyledActionWrapper>
);
};
ModalFooter.displayName = 'ModalFooter';
@@ -0,0 +1,44 @@
import styled from '@emotion/styled';
import styledCss from '@styled-system/css';
import type { LayoutProps, SpaceProps } from 'styled-system';
import { compose, layout, space } from 'styled-system';
import { JUSTIFY } from './constants';
import type { ActionsProps } from './types';
/** Стилизованный контейнер для кнопок. */
export const StyledActionWrapper = styled.div<LayoutProps & Pick<ActionsProps, 'actionsDirection' | 'align'> & SpaceProps>(
({ actionsDirection, align }) => {
const justifyContent = JUSTIFY[align!];
const actionsStyles =
actionsDirection === 'horizontal'
? {
justifyContent,
'& button+button': {
ml: 'modal.buttons.horizontal',
},
}
: {
'& button': {
width: '100%',
},
'& button+button[role="link"]': {
mt: 'modal.buttons.vertical.link',
},
'& button+button[role="button"]': {
mt: 'modal.buttons.vertical.button',
},
};
const flexDirectionModal = actionsDirection === 'horizontal' ? 'row' : 'column';
return styledCss({
justifyContent: 'center',
...actionsStyles,
display: 'flex',
flexDirection: flexDirectionModal,
gap: 'none',
});
},
compose(layout, space)
);
@@ -0,0 +1,65 @@
import type { MenuItemProps } from '@fractal-ui/composites';
import type { ButtonProps, ButtonLinkProps } from '@fractal-ui/core';
import type { PopupContainerProps } from '@fractal-ui/overlays';
import type { Direction } from '@fractal-ui/styling';
/** Расположение кнопок в модальном окне. */
export type ActionsDirection = Direction;
/** Свойства элементов выпадающего меню. */
type ContextMenuItemProps = Pick<MenuItemProps, 'onClick' | 'text'>;
/** Свойства кнопки модального окна. */
export type ModalButtonProps = {
/** Текст кнопки. */
text: string;
/**
* Флаг, запрещающий закрытие модального окна при клике на кнопку.
*
* Если true - модальное окно не закрывается.
*/
preventClose?: boolean;
} & (
| (ButtonLinkProps & {
/** Тип кнопки - ссылка. */
actionBtnType: 'link';
})
| (ButtonProps & {
/** Тип кнопки - кнопка с тултипом. */
actionBtnType: 'withTooltip';
/** Текст тултипа кнопки. */
tooltip: string;
})
| (ButtonProps & {
/** Тип кнопки - кнопка. */
actionBtnType: 'button';
})
| (Omit<ButtonProps, 'onClick'> & {
/** Тип кнопки - кнопка с выпадающем меню. */
actionBtnType: 'dropdown';
/** Эдементы выпадающего меню. */
items: ContextMenuItemProps[];
})
);
export type Align = 'center' | 'justify' | 'left' | 'right';
/** Свойства компонента обертки для экшенов модального окна. */
export interface ActionsProps extends Pick<PopupContainerProps, 'onClose'> {
/**
* Кнопка модального окна.
*/
actions: ModalButtonProps[];
/**
* Расположение кнопок.
*
* @default horizontal
*/
actionsDirection?: ActionsDirection;
/**
* Выравнивание кнопок.
*
* @default right
*/
align?: Align;
}
@@ -0,0 +1,8 @@
import styled from '@emotion/styled';
import { Text } from '@fractal-ui/styling';
import styledCss from '@styled-system/css';
import type { ColorProps } from 'styled-system';
import { color } from 'styled-system';
/** Текст с переносом строки. */
export const WrappedText = styled(Text.P2)<ColorProps>(styledCss({ whiteSpace: 'pre-wrap' }), color);
+56
View File
@@ -0,0 +1,56 @@
export const mockConfig = {
cryptoPluginAutoDetection: false,
cryptoPlugins: {
SFT: {
url: 'https://cm.sftcomp.ru:8899',
minVersion: '1.12.4',
},
BSS: {
url: 'https://bssplugin.bssys.com:11742',
minVersion: '3.20.2.2150',
},
},
cryptoPluginsInstaller: {
BSS: {
cdn: {
win: 'https://cdn.gbo.gazprombank.ru/win-download/BssPluginSetupGPB.exe',
nix: 'https://cdn.gbo.gazprombank.ru/win-download/BssPluginSetupGPB.exe',
astra: 'https://cdn.gbo.gazprombank.ru/nix-download/crypto-module.deb',
alt: 'https://cdn.gbo.gazprombank.ru/nix-download/crypto-module-alt.rpm',
red: 'https://cdn.gbo.gazprombank.ru/nix-download/crypto-module-redos.rpm',
},
proxy: {
win: '/win-download/BssPluginSetupGPB.exe',
nix: '/win-download/BssPluginSetupGPB.exe',
astra: 'https://gbo.gazprombank.ru/nix-download/crypto-module.deb',
alt: 'https://gbo.gazprombank.ru/nix-download/crypto-module-alt.rpm',
red: 'https://gbo.gazprombank.ru/nix-download/crypto-module-redos.rpm',
},
},
SFT: {
proxy: {
win: '/win-download/cryptomodule.exe',
nix: '/nix-download/crypto-module.jar',
astra: 'https://gbo.gazprombank.ru/nix-download/crypto-module.deb',
alt: 'https://gbo.gazprombank.ru/nix-download/crypto-module-alt.rpm',
red: 'https://gbo.gazprombank.ru/nix-download/crypto-module-redos.rpm',
},
cdn: {
win: '/',
nix: 'https://cdn.gbo.gazprombank.ru/nix-download/crypto-module.jar',
astra: 'https://cdn.gbo.gazprombank.ru/nix-download/crypto-module.deb',
alt: 'https://cdn.gbo.gazprombank.ru/nix-download/crypto-module-alt.rpm',
red: 'https://cdn.gbo.gazprombank.ru/nix-download/crypto-module-redos.rpm',
},
},
},
cryptoPluginByDefault: 'SFT',
cmLinuxInstructionUrl: 'https://client.stage.gboteam.ru/static/User_Guide_for_Linux_2.1_16.01.2024.pdf',
signReturnError: true,
cloudSign: {
signTime: 240,
enabled: true,
},
supportPhoneNumber: '8-800-100-11-89',
backendVerifySign: true,
};
+3
View File
@@ -0,0 +1,3 @@
export * from './observer';
export * from './config';
export * from './request';
+21
View File
@@ -0,0 +1,21 @@
export const mockObservers = () => {
const mockIntersectionObserver = jest.fn();
const mockResizeObserver = jest.fn();
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
});
mockResizeObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
});
// eslint-disable-next-line compat/compat
window.IntersectionObserver = mockIntersectionObserver;
// eslint-disable-next-line compat/compat
window.ResizeObserver = mockResizeObserver;
};
+11
View File
@@ -0,0 +1,11 @@
/* eslint-disable @eco/no-missing-localization */
export const installationCheck = {
url: '/api/v1/version',
method: 'GET',
status: 200,
response: {
data: {
data: '1.1.1',
},
},
};
+1
View File
@@ -0,0 +1 @@
export * from './signature-icon';
@@ -0,0 +1,46 @@
/* eslint-disable react/jsx-sort-props */
import React from 'react';
import type { IconComponent } from '@fractal-ui/library';
/**
* Компонент иконки SignaturePictogramIcon.
*/
export const SignaturePictogramIcon: IconComponent = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_3004_49207)">
<path
opacity="0.24"
fillRule="evenodd"
clipRule="evenodd"
d="M7.99973 15.8262C8.40957 15.7828 8.82321 15.629 9.20545 15.3399C9.86842 17.5542 13.1323 17.5542 13.7952 15.3399C14.1772 15.6288 14.5905 15.7825 15 15.8261V24L11.4999 21.2399L7.99973 24V15.8262Z"
fill="url(#paint0_linear_3004_49207)"
/>
<path
opacity="0.6"
d="M18.946 8.50032C20.9349 7.17176 19.9149 4.2194 17.518 4.26861C18.334 2.10355 15.6821 0.233728 13.7952 1.6607C13.1323 -0.553566 9.86841 -0.553566 9.20544 1.6607C7.26753 0.233728 4.66666 2.10355 5.48262 4.26861C3.08573 4.2194 2.06578 7.17176 4.05469 8.50032C2.06578 9.82888 3.08573 12.7812 5.48262 12.732C4.66666 14.8971 7.31853 16.7669 9.20544 15.3399C9.86841 17.5542 13.1323 17.5542 13.7952 15.3399C15.6821 16.7669 18.334 14.8971 17.518 12.732C19.9149 12.7812 20.9349 9.82888 18.946 8.50032Z"
fill="url(#paint1_linear_3004_49207)"
/>
<path
d="M11.5003 14.0005C14.538 14.0005 17.0005 11.538 17.0005 8.50033C17.0005 5.46265 14.538 3.00012 11.5003 3.00012C8.46265 3.00012 6.00012 5.46265 6.00012 8.50033C6.00012 11.538 8.46265 14.0005 11.5003 14.0005Z"
fill="url(#paint2_linear_3004_49207)"
/>
</g>
<defs>
<linearGradient id="paint0_linear_3004_49207" x1="11.4999" y1="15.3399" x2="11.4999" y2="23.6062" gradientUnits="userSpaceOnUse">
<stop stopColor="white" stopOpacity="0.6" />
<stop offset="1" stopColor="white" />
</linearGradient>
<linearGradient id="paint1_linear_3004_49207" x1="11.5003" y1="0" x2="11.5003" y2="16.2279" gradientUnits="userSpaceOnUse">
<stop stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0.6" />
</linearGradient>
<linearGradient id="paint2_linear_3004_49207" x1="11.5003" y1="3.00012" x2="11.5003" y2="13.5005" gradientUnits="userSpaceOnUse">
<stop stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0.6" />
</linearGradient>
<clipPath id="clip0_3004_49207">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);
+7
View File
@@ -0,0 +1,7 @@
export * from './stream-constants';
export * from './interfaces';
export * from './utils';
export * from './services';
export * from './components';
export * from './icons';
export * from './helpers';
+98
View File
@@ -0,0 +1,98 @@
/**
* Подписываемые данные.
*/
export interface ICloudSignData {
/**
* Данные.
*/
data: string;
/**
* Наименование.
*/
name: string;
/**
* Идентификатор.
*/
id: string;
/**
* Идентификатр документа.
*/
documentId: string;
}
/**
* Подписанные данные.
*/
export interface ICloudSignDocument {
/**
* Идентификатор подписи.
*/
documentId: string;
/**
* Подпись.
*/
signature: string;
/**
* Дата подписи.
*/
signedAt: string;
/**
* Статус.
*/
status: 'ERROR' | 'NOT_SIGNED' | 'SIGN_NOT_CONFIRMED' | 'SIGNED';
}
/**
* Результат подписи.
*/
export interface ICloudSignResult {
/**
* Документы.
*/
documents: ICloudSignDocument[];
/**
* Идентификатор.
*/
id: string;
/**
* Результат.
*/
result: 'ERROR' | 'SUCCESS' | 'UNKNOWN';
/**
* Описание ошибки.
*/
errorText?: string;
}
/**
* Ответ при подписи.
*/
export interface ICloudSignResponse<T> {
/**
* Данные.
*/
data: T;
/**
* Ошибка.
*/
error: {
/**
* Код.
*/
code: string;
/**
* Сообщение.
*/
message: string;
};
}
/**
* Операция облачной подписи.
*/
export interface ICloudSignOperation {
/**
* Идентификатор операции.
*/
operationId: string;
}
+68
View File
@@ -0,0 +1,68 @@
import type { ERROR } from '../stream-constants';
/**
* Интерфейс стандартной серверной ошибки.
*/
export interface IServerError<TErrorCode> {
/**
* Код ошибки.
*/
code: TErrorCode;
/**
* Сообщение об ошибке.
*/
message: string;
}
/**
* Интерфейс ответа сервера.
* Ошибки, произошедшие на сервере, передаются в поле errorInfo и имеют код типа ERROR.
*/
export interface IServerResp<T, TErrorCode = ERROR> {
/**
* Содержимое ответа.
*/
data: T;
/**
* Информация об ошибке.
*/
errorInfo?: IServerError<TErrorCode>;
}
/**
* Интерфейс ответа сервера.
* Ошибки, произошедшие на сервере, передаются в поле error и имеют код типа string.
* */
export interface IServerDataResp<T> {
/**
* Содержимое ответа.
* */
data: T;
/**
* Информация об ошибке.
* */
error?: IServerError<string>;
}
/**
* Инсталлеры КМ.
*/
export type Installer = { [key in 'alt' | 'astra' | 'mac' | 'nix' | 'red' | 'win']: string };
/**
* Интерфейс ответа сервера. Без обертки ошибки в errorInfo.
*/
export interface IDataResponse<T> {
/**
* Содержимое ответа.
*/
data: T;
/**
* Код ошибки.
*/
code: number;
/**
* Сообщение об ошибке.
*/
message?: string;
}
@@ -0,0 +1,263 @@
import type { CONTAINER_TYPE, CRYPTO_MODULE_SIGN_TYPE } from '../stream-constants/olk';
import type { Installer } from './common';
/**
* Ошибка КМ.
*/
export interface IError {
/**
* Код.
*/
code: number;
/**
* Заголовок ошибки.
*/
text: string;
/**
* Ссылка на ошибку.
*/
link?: string;
/**
* Описание ошибки.
*/
description?: string;
}
export interface IIdentityFields {
commonName?: string;
organizationalUnit?: string;
organization?: string;
country?: string;
state?: string;
locality?: string;
inn?: string;
ogrn?: string;
snils?: string;
ogrnip?: string;
email?: string;
principalAttrsAsString?: string;
}
export interface IGPBCryptoModuleSubject {
commonName?: string;
organization?: string;
country?: string;
state?: string;
locality?: string;
identityFields?: IIdentityFields;
}
export interface IGPBCryptoModuleIssuer {
commonName?: string;
identityFields?: IIdentityFields;
}
export interface IGPBCryptoModuleCheckResponseData {
serialNumber?: string;
subject?: IGPBCryptoModuleSubject;
issuer?: IGPBCryptoModuleIssuer;
validFrom: number;
validUntil: number;
publicKey: string;
body: string;
}
export interface IGPBCryptoModuleSign {
/**
* Идентификатор локали, по умолчанию ru_RU.
*/
lcid?: string;
/**
* Идентификатор сессии для контейнера ключей.
*/
keySessID: string;
/**
* Тип подписи.
*/
signType: CRYPTO_MODULE_SIGN_TYPE;
/**
* Массив данных для подписи в кодировке Base64.
*/
data: string[];
/**
* Признак открепления подписи.
*/
detached?: boolean;
/**
* Исходные данные – ранее вычисленный хэш, предварительное хэширование перед подписью не требуется.
*/
calculatedHash?: boolean;
}
export interface IGPBCryptoModuleSignResult {
data: string;
signType: CRYPTO_MODULE_SIGN_TYPE;
}
export interface IGPBCryptoModuleCheck {
/**
* Информация о требуемом провайдере.
*/
providerInfo?: ProviderInfo;
/**
* Сертификат.
*/
certificate: string;
/**
* Данные для подписи в кодировке Base64.
*/
data: string[];
/**
* Массив подписанных данных.
*/
signedData: IGPBCryptoModuleSignResult[];
/**
* Массив сертификатов УЦ.
*/
certificatesCA?: string[];
/**
* СОС.
*/
CRLs?: string[];
/**
* Исходные данные – ранее вычисленный хэш, предварительное хэширование перед проверкой не требуется.
*/
calculatedHash?: boolean;
}
/**
* Криптопровайдер.
*/
export interface ProviderInfo {
/**
* Идентификатор интерфейса.
*
* @description В случае если не указан, используется значение по-умолчанию. Для Windows — "CAPI", для прочих OS "JCA".
*/
providerInterface?: PROVIDER_INTERFACE;
/**
* Наименование криптопровайдера.
*/
providerName: string;
/**
* Тип провайдера.
*
* @description Заполняется для "CAPI".
*/
providerType?: number;
/**
* Алгоритм криптопровадера.
*
* @description Заполняется для "JCP".
*/
providerAlhoritm?: string;
}
/**
* Идентификатор интерфейса криптопровайдера.
*/
export enum PROVIDER_INTERFACE {
CAPI = 'CAPI',
JCA = 'JCA',
SCOM_MESPRO_4 = 'SCOM_MESPRO_4',
}
export interface IGPBCryptoModuleCheckResponse {
error: IError;
data: IGPBCryptoModuleCheckResponseData;
}
export interface IContainerDboBss {
'@type': CONTAINER_TYPE;
containerLocationName: string;
}
export interface IContainerSCOMNative {
'@type': CONTAINER_TYPE;
/**
* Путь к файлу с ключом.
*/
keyFile: string;
/**
* Директория с ключами пользователя.
*/
keyPath: string;
/**
* Путь к файлу сертификата.
*/
certPath: string;
/**
* CA сертификаты.
*/
CAFiles: any;
/**
* CRL файлы.
*/
CRLFiles: any;
}
export interface IGPBCryptoModuleInitkey {
/**
* Идентификатор локали, по умолчанию ru_RU.
*/
lcid?: string;
/**
* Информация о требуемом провайдере.
*/
providerInfo?: ProviderInfo;
/**
* Сертификат в формате X509, в кодировке Base64.
*/
X509data: string;
/**
* Информация о требуемом контейнере.
*/
containerInfo?: IContainerDboBss | IContainerSCOMNative;
/**
* Данные на подпись для аутентификации.
*/
authData?: string;
/**
* Тип подписи.
*/
signType: CRYPTO_MODULE_SIGN_TYPE;
}
export interface IGPBCryptoModuleResultInitKey {
/**
* Подписанные данные в кодировке base64.
*/
signData: string;
/**
* Идентификатор сессии для контейнера ключей.
*/
keySessID: string;
}
/**
* Конфиг криптоплагина.
*/
export interface ICryptoPlugin {
/**
* URL доступа к криптоплагину.
*/
url: string;
/**
* Минимальная версия криптоплагина.
*/
minVersion: string;
}
/**
* Url адреса установщика криптоплагина.
*/
export interface ICryptoPluginsInstaller {
/**
* CDN url.
*/
cdn: Installer;
/**
* Proxy url.
*/
proxy: Installer;
}
+363
View File
@@ -0,0 +1,363 @@
import type { PROFILE_AUTH_METHOD, SIGN_KIND, CHECK_STATUS } from '../stream-constants';
import type { IError, IGPBCryptoModuleCheckResponseData } from './crypto-module';
import type { DST_STATUS, IUniversalSignEntity, UserType } from './entities';
import type { ISignParams } from './sign-modal';
/**
* Данные для функции подписи v2.2.
*/
export interface IUniversalSignV2Response {
/**
* Подписываемые сущности.
*/
signDocuments: IUniversalSignEntity[];
}
/**
* СЭП для универсальной функции электронной подписи.
*/
export interface IDstForSign {
/** Удостоверяющий центр. */
certificateAuthority: string;
/** Данные сертификата. */
certificateData: string;
/** Идентификатор сертификата. */
certificateId: string;
/** Владелец сертификата. */
certificateOwner: string;
/** Идентификатор СЭП. */
dstId: string;
/** Статус сертификата. */
dstStatus: DST_STATUS;
/** Срок действия. */
expirationDate: string;
/** Наименование организации. */
organizationName: string;
/** Вид ЭП. */
signKind: SIGN_KIND;
/** Имя и расположение ключевого контейнера. */
containerNameLocation: string;
/** Метод вторичной аутентификации. */
profileDssSecondaryAuthMethod?: PROFILE_AUTH_METHOD;
/**
* Идентификатор организации, в которой используется СЭП.
*/
clientId: string;
/**
* Признак установленного пин-кода.
*/
pinCodeEnabled?: boolean;
}
/**
* Данные для функции подписи v2.1.
*/
export interface IUniversalSignResponse {
/**
* Список сертификатов для подписи.
*/
certificates: IUserCertificate[];
/**
* Подписываемые сущности.
*/
signDocuments: IUniversalSignEntity[];
}
export interface IUserCertificate {
absId: string;
/**
* Base64 представление сертификата.
*/
data: string;
/**
* Идентификатор сертификата.
*/
id: string;
organizationName: string;
/**
* Серийный номер.
*/
serialNumber: string;
/**
* Действует с.
*/
validFrom: string;
/**
* Действует по.
*/
validTo: string;
/**
* Источник сертификата.
*/
origin: string;
/**
* Идентификатор пользователя в UAA.
*/
userId: string;
/**
* ФИО владельца сертификата.
*/
userName: string;
/**
* Параметры подписи.
*/
signParams: string;
/**
* Имя и расположение ключевого контейнера.
*/
containerNameLocation: string;
/**
* ФИО владельца сертификата.
*/
// TODO после перехода стримов на универсальную подпись скорее всего упразднятся
ownerName?: string;
/**
* Издатель сертификата.
*/
issuerName?: string;
/**
* Владелец сертификата.
*/
subject?: string;
/**
* Издатель сертификата.
*/
issuedBy?: string;
/**
* Идентификатор сертификата пользователя банка.
*/
bankUserCertificateId?: string;
/**
* ID облачного СЭП.
*/
cloudDstId?: string;
/**
* Вид ЭП.
*/
signKind?: SIGN_KIND;
}
/**
* Интерфейс крипто модуля.
*/
export interface ICryptoModule {
/**
* Функция инициализации.
*/
init(options?: any): Promise<any>;
/**
* Функция подписи.
*/
batchSign: BatchSignFunc;
/**
* Функция проверки подписи.
*/
verifySign: VerifySignFunc;
/**
* Функция проверки установлен ли крипто модуль.
*/
installationCheck(): Promise<any>;
}
/**
* Функция подписи.
*
* @param thumbprint Хеш сертификата, которым подписываем.
* @param data Подписываемые данные.
* @param sessId Сессия подписи.
* @param detached Признак открепления подписи.
* @param calculatedHash Признак, что значение - ранее вычисленный хэш.
* @returns Строка подписи.
*/
export type BatchSignFunc = (
thumbprint: string,
data: string[],
sessId?: string,
detached?: boolean,
calculatedHash?: boolean,
signType?: ISignParams['signType']
) => Promise<string[]>;
/**
* Функция проверки подписи.
*
* @param signature Хеш подписи.
* @param data Подписываемые данные (дайджесты).
* @param certificate Сертификат, которым были подписаны данные.
* @param certificatesCA Список УЦ для данного сертификата.
* @param crls Список СОС для данного сертификата.
* @param providerName Имя провайдера.
* @param calculatedHash Признак, что значение - ранее вычисленный хэш.
* @returns Строка подписи.
*/
export type VerifySignFunc = ({
signature,
data,
certificate,
certificatesCA,
crls,
providerName,
calculatedHash,
}: IVerifySignFunc) => Promise<IVerifySignResult>;
/**
* Аргументы функции проверки подписи.
*/
export interface IVerifySignFunc {
/** Хеш подписи. */
signature: string;
/** Подписываемые данные (дайджесты). */
data: string[];
/** Сертификат, которым были подписаны данные. */
certificate: string;
/** Список УЦ для данного сертификата. */
certificatesCA: string[];
/** Список СОС для данного сертификата. */
crls: string[];
/** Имя провайдера. */
providerName?: string;
/** Признак, что значение - ранее вычисленный хэш. */
calculatedHash?: boolean;
}
/**
* Интерфейс проверки валидности крипты.
*/
export interface IVerifySignResult {
/**
* Данные проверки.
*/
data?: IGPBCryptoModuleCheckResponseData;
/**
* Статус проверки.
*/
status: CHECK_STATUS;
/**
* Ошибка проверки.
*/
error?: IError;
}
/**
* Сертификат пользователя банка.
*/
export interface IBankUserCertificate {
/**
* Тип сертификата.
*/
signKind: SIGN_KIND.NOT_QUALIFIED_SIGN | SIGN_KIND.QUALIFIED_SIGN;
/**
* Ссылка на сущность “Сертификат электронной подписи”.
*/
certificateId: string;
/**
* Сертификат.
*/
certificate: IUserCertificate;
/**
* Наименование сертификата.
*/
certificateName: string;
/**
* Комментарий для пользователя Банка.
*/
comment: string;
/**
* Идентификатор.
*/
id: string;
/**
* ФИО владельца сертификата.
*/
ownerName: string;
/**
* Статус.
*/
status: 'ACTIVE' | 'NEW' | 'PROCESSING' | 'REJECTED';
/**
* Ссылка на сущность “Пользователь”.
*/
userId: string;
/**
* Наименование эмитента.
*/
issuerName: string;
/**
* Дата начала действия сертификата.
*/
validFrom: string;
/**
* Дата окончания действия сертификата.
*/
validTo: string;
}
export enum VALIDATE_SIGN_ERRORS {
CANT_RETRIEVE_INFO_ABOUT_SIGNER_OR_CERTIFICATE = 'CANT_RETRIEVE_INFO_ABOUT_SIGNER_OR_CERTIFICATE',
}
/**
* Дополнительная информация о подписи.
*/
export interface IAdditionalVerifySignData {
/**
* УЦ.
*/
caCertificates: string[];
/**
* СОС.
*/
revokedCertificates: string[];
/**
* Информация о пользователе.
*/
signerInfo: {
/**
* Фамилия.
*/
familyName: string;
/**
* Имя.
*/
firstName: string;
/**
* Отчество.
*/
middleName: string;
/**
* Тип пользователя.
*/
type: UserType;
/**
* Идентификатор пользователя.
*/
uaaId: string;
};
/**
* Дата проверки подписи.
*/
signDate: string;
/**
* Идентификатор подписи.
*/
signId: string;
/**
* Ошибка.
*/
error: VALIDATE_SIGN_ERRORS;
/**
* Информация о провайдере.
*/
providerInfo: string;
}
+387
View File
@@ -0,0 +1,387 @@
import type { SCOPE } from '../utils/scope';
/**
* Базовый интерфейс сущности.
*/
export interface IBaseEntity {
/**
* Идентификатор.
*/
id: string;
}
/**
* Подписываемая сущность для компонента универсальной подписи.
*/
export interface IUniversalSignEntity {
/**
* Идентификатор клиента банка.
*/
clientId: string;
/**
* Локальный идентификатор документа для связи с исходящими параметрами.
*/
documentId: string;
/**
* Набор подписываемых данных.
*/
signDataList: IUniversalSignData[];
}
/**
* Подписываемые данные для компонента универсальной подписи.
*/
export interface IUniversalSignData {
/**
* Идентификатор набора подписываемых данных для связи с исходящими параметрами функции. Например, для ДСФ:
Документ ДСФ - заполняется идентфикатором документа;
Вложение в ДСФ - заполняется идентфикатором вложения.
*/
id: string;
/**
* Наименование подписываемых данных для отображения в диалоге подписи.
*/
label: string;
/**
* Наименование подписываемых данных для отображения в облачной подписи.
*/
dssLabel: string;
/**
* Данные, которые будут подписаны ЭП.
*/
digest: string;
/**
* Тип подписываемых данных: документ, вложение и т.п.
*/
type?: SIGN_DATA_TYPE;
/**
* Блок отображаемых данных на форме подписи.
*/
signDataViewList: IUniversalSignDetails[];
}
/**
* Тип подписываемого документа.
*/
export enum SIGN_DATA_TYPE {
/**
* Документ.
*/
DOCUMENT = 'DOCUMENT',
/**
* Вложение.
*/
ATTACHMENT = 'ATTACHMENT',
}
/**
* Общий код завершения подписи документов.
*/
export enum SIGN_RESULT_CODE {
/**
* Операция подписи хотя бы одного документа завершилась корректно.
*/
SUCCESS = 'SUCCESS',
/**
* Пользователь отказался от установки подписи
* или
* возникла ошибка при подписи одного из наборов данных документа.
*/
ERROR = 'ERROR',
}
/**
* Блок отображаемых данных на форме подписи.
*/
export interface IUniversalSignDetails {
/**
* Наименование блока подписываемых данных.
*/
label: string;
/**
* Подписываемые поля.
*/
signFieldsViewList: IOption[];
}
/**
* Набор свойства подписываемых данных.
*/
export interface IOption<T = any> {
/**
* Лейбл поля.
*/
label: string;
/**
* Значение поля.
*/
value: T;
/**
* Занчение-ключ поля.
*/
key?: string;
}
/**
* Тип пользователя.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum UserType {
/**
* Банковский сотрудник.
*/
BANK = 'BANK',
/**
* Клиент.
*/
CLIENT = 'CLIENT',
/**
* Технический.
*/
TECHNICAL = 'TECHNICAL',
}
/**
* Результат подписи для компонента универсальной подписи.
*/
export interface IUniversalSignResultSummary {
/**
* Общий код завершения.
*/
resultCode: SIGN_RESULT_CODE;
/**
* Если <Общий код завершения> не равен 0, то заполняется текстом ошибки, иначе не заполняется.
*/
error?: string;
/**
* Код ошибки.
*/
errorCode?: string;
/**
* Если <Общий код завершения> не равен 0, то заполняется локальным идентификатором сертификата из входящих параметров функции,
* использованный для подписи, иначе не заполняется.
*/
certificateId?: string;
/**
* Набор обработанных документов.
*/
sign: IUniversalSignResultEntity[];
/**
* Ошибка КМ.
*/
cryptoError?: any;
}
/**
* Результат подписи документа.
*/
export interface IUniversalSignResultEntity {
/**
* Идентификатор документа для связи с входящими параметрами.
*/
documentId: string;
/**
* Код завершения.
*/
resultCode: SIGN_RESULT_CODE;
/**
* Если <Код завершения> не равен 0, то заполняется текстом ошибки, иначе не заполняется.
*/
error?: string;
/**
* Набор подписанных данных.
*/
signDataResult?: IUniversalSignDataResult[];
}
/**
* Подписанная сущность.
*/
export interface IUniversalSignDataResult {
/**
* Локальный идентификатор набора подписываемых данных для связи с входящими параметрами.
*/
id: string;
/**
* Подписанные данные.
*/
digest: string;
/**
* Значение подписи.
*/
signature: string;
/**
* Дата и время подписи.
*/
signDate: string;
/**
* Тип сущности. Нужен на стороне прикладного стрима для понимания как сохранять результат подписи.
*/
type?: SIGN_DATA_TYPE;
}
export interface IDocumentAttachment {
/**
* Токен.
*/
accessToken?: string;
/**
* Идентификатор вложения.
*/
attachmentId?: string;
/**
* Хэш файла.
*/
fileHash?: string;
/**
* Наименование файла.
*/
fileName?: string;
/**
* Размер файла.
*/
fileSize?: number;
}
/**
* Тип вложения.
*/
export enum ATTACHMENT_TYPES {
PDF = 'PDF',
DOC = 'DOC',
DOCX = 'DOCX',
}
/**
* Статус сертификата.
*/
export enum DST_STATUS {
ACTIVE = 'ACTIVE',
EXPIRED = 'EXPIRED',
}
/**
* Подписанное вложение.
*/
export interface ISignedAttachment extends IDocumentAttachment {
/**
* Идентификатор.
*/
id: string;
/**
* Тип вложения.
*/
contentType: string;
/**
* Расширение файла.
*/
extension: ATTACHMENT_TYPES;
/**
* Ид владелеца.
*/
owner: string;
/**
* Ид автора.
*/
author: string;
/**
* ФИО автора.
*/
authorFio: string;
/**
* Дата прикрепления.
*/
attachedAt: string;
/**
* Список подписей.
*/
signatures: ISignatureDoc[];
/**
* Место хранения файла.
*/
side: SCOPE;
}
export type SourceSystem = 'DIRECT_1C' | 'IMPORT_SERVICE' | 'LK_UVED' | 'MBC' | 'WEB';
export interface ISignatureDoc {
certificate: {
/**
* Идентификатор сертификата.
*/
id: string;
/**
* Base64 представление сертификата.
*/
value: string;
/**
* Серийный номер.
*/
serialNumber: string;
/**
* Кому выдан сертификат.
*/
subject: string;
/**
* Период действия с.
*/
validFrom: string;
/**
* Период действия по.
*/
validTo: string;
};
/**
* Версия СПП.
*/
sppVersion?: number;
/**
* Дата подписания.
*/
signedAt: string;
/**
* Идентификатор.
*/
id: string;
/**
* Идентификатор владельца.
*/
ownerId: string;
/**
* Идентификатор типа подписи.
*/
typeId: string;
value: {
/**
* Дайджест.
*/
digest: string;
/**
* Подпись.
*/
signature: string;
};
/**
* Источник подписи.
*/
sourceSystem?: SourceSystem;
}
/**
* Свойства подписанного документа.
*/
export interface IBaseSignedDocument extends IBaseEntity {
/**
* Список подписей.
*/
signatures: ISignatureDoc[];
/**
* Список вложений.
*/
attachments?: ISignedAttachment[];
/**
* Источник подписи.
*/
docSourceSystem?: SourceSystem;
}
+14
View File
@@ -0,0 +1,14 @@
export * from './sign';
export * from './sign-modal';
export * from './entities';
export * from './crypto';
export * from './signature';
export * from './crypto-module';
export * from './olk';
export * from './cloud';
export * from './common';
export * from './signature';
export * from './mobile';
export * from './settings';
export * from './org-cert';
export * from './user';
+53
View File
@@ -0,0 +1,53 @@
/**
* Информация о ios приложении.
*/
export interface IosApp {
/**
* Наименование приложения.
*/
name: string;
/**
* ID приложения.
*/
id: string;
}
/**
* Информация о android приложении.
*/
export interface AndroidApp {
/**
* Наименование приложения.
*/
name: string;
/**
* ID приложения.
*/
id: string;
/**
* Хост для открытия приложения.
*/
host?: string;
/**
* Наименование приложения.
*/
packageName?: string;
/**
* Схема для открытия приложения.
*/
scheme?: string;
}
/**
* Мобильное приложение.
*/
export interface MobileApp {
/**
* Ios приложение.
*/
ios: IosApp;
/**
* Android приложение.
*/
android: AndroidApp;
}
+74
View File
@@ -0,0 +1,74 @@
/**
* Информация об ошибке.
*/
export interface ICryptoError {
/**
* Код.
*/
code: number;
/**
* Сообщение.
*/
text: string;
}
/**
* Ошибка криптомодуля.
*/
export interface ICryptoModuleError<T> {
/**
* Данные.
*/
data: T;
/**
* Информация об ошибке.
*/
errorInfo: Pick<ICryptoError, 'code'>;
}
/**
* Данные об ошибке от криптомодуля.
*/
export interface ICryptoModuleErrorData<T> {
data: ICryptoModuleError<T>;
}
/**
* Ответ с ошибкой от криптомодуля.
*/
export interface ICryptoModuleErrorResponse<T> {
/**
* Ответ.
*/
response: ICryptoModuleErrorData<T>;
}
/**
* Опции инициализации ключа.
*/
export interface IOlkInitOptions {
/**
* Сертификат.
*/
certData: string;
/**
* Расположение контейнера.
*/
containerNameLocation: string;
}
export enum VERIFY_SIGN_ERROR {
/**
* Ошибка проверки подписи.
*/
SIGN_NOT_CORRECT = 200,
/**
* Ошибка при проверке цепочки сертификатов.
*/
VERIFYING_CERTIFICATE_CHAIN = 201,
/**
* Неверный сертификат подписи.
*/
INVALID_SIGN_CERTIFICATE = 202,
UNKNOWD = 'UNKNOWN', // для крайних случаев
}
+177
View File
@@ -0,0 +1,177 @@
export interface IOrgCertCheckResult {
title: string;
description: string;
data: any;
code: string;
}
/**
* Информация о сертификате.
*/
export interface IOrgCertFileInfo {
/**
* Наименование центра сертификации (Издателя).
*/
issuer: string;
/**
* Наименование организации.
*/
organizationName: string;
/**
* Период действия с.
*/
validFrom?: string;
/**
* Период действия по.
*/
validTo?: string;
/**
* ФИО владельца сертификата.
*/
whomIssued: string;
/**
* Серийный номер.
*/
serialNumber?: string;
/**
* Отпечаток.
*/
thumbprint: string;
/**
* Значение поля SUBJECT CN из сертификата.
*/
subjectCommonName?: string;
/**
* Наименование организации.
*/
subjectOrganization?: string;
/**
* Значение поля SUBJECT OU из сертификата.
*/
subjectOrganizationalUnit?: string;
/**
* Значение поля SUBJECT L из сертификата.
*/
subjectLocality?: string;
/**
* Значение поля SUBJECT C из сертификата.
*/
subjectCountry?: string;
/**
* Значение поля SUBJECT E из сертификата.
*/
subjectEmail?: string;
/**
* Значение поля SUBJECT G из сертификата.
*/
subjectGivenName?: string;
/**
* Значение поля SUBJECT SN из сертификата.
*/
subjectSurname?: string;
/**
* Должность.
*/
subjectTitle?: string;
/**
* ИНН организации.
*/
subjectInn?: string;
/**
* ОГРН организации.
*/
subjectOgrn?: string;
/**
* ОГРНИП организации.
*/
subjectOgrnIp?: string;
}
export interface ICertResponse<T> {
code: number;
message: string;
data: T & { errorMessage?: string };
}
export interface ICertParseResponse {
/**
* Cерийный номер сертификата центра сертификации (Издателя).
*/
authorityCertificateSerial: string;
/**
* Идентификатор ключа центра сертификации (Издателя).
*/
authorityKeyId: string;
/**
* Наименование центра сертификации (Издателя).
*/
authorityName: string;
/**
* Отпечаток.
*/
footprint: string;
/**
* Предназначение ключа.
*/
keyUsage: boolean[];
/**
* Серийный номер.
*/
serialNumber: string;
/**
* Средство подписи сертификата.
*/
signTool: string;
/**
* Значение поля SUBJECT CN из сертификата.
*/
subject: string;
/**
* Значение поля SUBJECT G из сертификата.
*/
subjectGivenName: string;
/**
* ИНН организации.
*/
subjectInn: string;
/**
* Наименование организации.
*/
subjectOrganization: string;
/**
* Значение поля SUBJECT SN из сертификата.
*/
subjectSurname: string;
/**
* Должность.
*/
subjectTitle: string;
/**
* Значение поля SUBJECT OU из сертификата.
*/
subjectOrganizationalUnit: string;
/**
* Значение поля SUBJECT L из сертификата.
*/
subjectLocality: string;
/**
* Значение поля SUBJECT C из сертификата.
*/
subjectCountry: string;
/**
* Значение поля SUBJECT E из сертификата.
*/
subjectEmail: string;
/**
* Период действия с.
*/
validFrom: string;
/**
* Период действия по.
*/
validTo: string;
/**
* ОГРН организации.
*/
subjectOgrn: string;
}
+172
View File
@@ -0,0 +1,172 @@
import type { CRYPTO_PLUGIN_TYPE } from '../stream-constants';
import type { ICryptoPlugin, ICryptoPluginsInstaller } from './crypto-module';
/**
* Код настройки.
*/
export enum SETTING_CODE {
/**
* Настройки.
*/
SETTINGS = 'SETTINGS',
/**
* Найстройки помощи.
*/
HELP_SETTINGS = 'HELP_SETTINGS',
/**
* Настройка CERT_ENROLL.
*/
CERT_ENROLL = 'CERT_ENROLL',
}
/**
* Тип настройки.
*/
export enum SETTING_TYPE {
/**
* Публичная настройка.
*/
PUBLIC = 'PUBLIC',
/**
* Публичная настройка для банка.
*/
BANK_PUBLIC = 'BANK_PUBLIC',
/**
* Клиентская настройка.
*/
CLIENT = 'CLIENT',
/**
* Банковская настройка.
*/
BANK = 'BANK',
}
/**
* Свойства облачной подписи.
*/
export interface ICloudSign {
/**
* Время ожидании подписи.
*/
signTime: number;
}
/**
* Элемент настройки.
*/
export interface ISetting {
/**
* Описание настройки.
*/
codeDescription: string;
/**
* Код настройки.
*/
code: SETTING_CODE;
/**
* ID настройки.
*/
id: string;
/**
* Тип настройки.
*/
settingType: SETTING_TYPE;
/**
* Наименование настройки.
*/
title: string;
/**
* ID пользователя.
*/
userId?: string;
/**
* Логическое значение настройки.
*/
valueBool?: boolean;
/**
* Денежное значение настройки.
*/
valueCurrency?: string;
/**
* Целочисленное значение настройки.
*/
valueLong?: number;
/**
* Строковое значение настройки.
*/
valueStr?: string;
}
export type ILoader = (code: string) => Promise<ISetting>;
/**
* Конфиг с настройками.
*/
export interface ISettingsConfig {
/**
* Признак автоопределения плагина.
*/
cryptoPluginAutoDetection: boolean;
/**
* Информация о криптоплагинах.
*/
cryptoPlugins: Record<CRYPTO_PLUGIN_TYPE, ICryptoPlugin>;
/**
* Информация об установочных файлах криптоплагинов.
*/
cryptoPluginsInstaller: Record<CRYPTO_PLUGIN_TYPE, ICryptoPluginsInstaller>;
/**
* Криптоплагин по-умолчанию.
*/
cryptoPluginByDefault: CRYPTO_PLUGIN_TYPE;
/**
* Ссылка на инструкцию для КМ Linux.
*/
cmLinuxInstructionUrl: string;
/**
* Флаг возврата ошибки из УП 2.2.
*/
signReturnError: boolean;
/**
* Свойства облачной подписи.
*/
cloudSign: ICloudSign;
/**
* Телефон технической поддержки.
*/
supportPhoneNumber: string;
/**
* Флаг использования проверки подписи через бек.
*/
backendVerifySign: boolean;
/**
* Условие, согласно которому на ЭФ выпуска/перевыпуска/подписи облачной ЭП отображаются тексты без конкретных наименований мобильного приложения для подтверждения операций.
* Если признак установлен в "false", то в текстах будут фигурировать формулировки с наименованием приложения "Бизнес Подпись".
*/
universalText: boolean;
/**
* Флаг доступности скачивания КМ под Mac.
*/
cmForMacEnabled: boolean;
}
/**
* Конфиг с настройками HELP.
*/
export interface IHelpConfig {
/**
* Урл страницы диагностики.
*/
startDiagnosticsUrl: string;
}
/**
* Интерфейс конфига Cert Enroll.
*/
export interface ICertEnrollConfig {
/**
* Условие, согласно которому на ЭФ выпуска/перевыпуска/подписи облачной ЭП отображаются тексты без конкретных наименований мобильного приложения для подтверждения операций.
* Если признак установлен в "false", то в текстах будут фигурировать формулировки с наименованием приложения "Бизнес Подпись".
*/
universalText: boolean;
}
@@ -0,0 +1,20 @@
import type { CRYPTO_MODULE_SIGN_TYPE } from '../stream-constants';
/**
* Параметры подписи.
*/
export interface ISignParams {
/**
* Признак открепления подписи.
*/
detached?: boolean;
/**
* Параметр расчета хэша подписи.
*/
calculatedHash?: boolean;
/**
* Формат подписи.
* Если вызывающему сервису требовается подпись с меткой времени, то в данном параметре следует указать 'CAdES_X_Long_Type_1'.
*/
signType?: CRYPTO_MODULE_SIGN_TYPE.CADES_X_LONG_TYPE_1 | CRYPTO_MODULE_SIGN_TYPE.CMS;
}
+99
View File
@@ -0,0 +1,99 @@
import type { SIGN_KIND } from '../stream-constants';
import type { IBaseEntity } from './entities';
/**
* Параметры запроса метода подписи.
*/
export interface ISignRequestData {
/**
* Идентификатор.
*/
id?: string;
/**
* ID сертификата.
*/
certificateId: string;
/**
* Дайджест.
*/
digest: string;
/**
* ID сущности.
*/
documentId: string;
signTypeId: string;
/**
* Значение подписи.
*/
signature: string;
}
/**
* Формат ответа запроса дайджеста.
*/
export interface IDigestResult {
/** Дайджест. */
value: string;
/** Версия. */
version: number;
}
/**
* Список документов на подпись.
*/
export interface RegistrySingDocuments extends IBaseEntity, Partial<Pick<IDigestResult, 'version'>> {
/**
* Список видов СЭП, которыми можно подписать документ.
*/
allowedDstKinds?: SIGN_KIND[];
}
/**
* Документы реестры.
*/
export interface IRegistryDocument {
/**
* Идентификатор клиента.
*/
clientId: string;
/**
* Наименование организации.
*/
clientName?: string;
/**
* Список документов на подпись.
*/
documents: RegistrySingDocuments[];
}
/**
* Статистика реестра.
*/
export interface IRegistryStatistics {
/**
* Количество документов.
*/
documentsCount: number;
/**
* Сумма документов.
*/
documentsSum: number;
/**
* Количество организаций.
*/
clientsCount: number;
}
/**
* Реестровая подпись.
*/
export interface IRegistrySign {
/**
* Документы реестры.
*/
documentsRegistry: IRegistryDocument[];
/**
* Статистика реестра.
*/
statistics?: IRegistryStatistics;
}
@@ -0,0 +1,112 @@
import type { PIN_CODE_CHECK } from '../stream-constants/sign';
import type { IServerResp } from './common';
import type { IUniversalSignResponse, IUniversalSignV2Response } from './crypto';
import type { IBaseEntity, UserType } from './entities';
import type { ISignRequestData } from './sign';
/**
* Данные о подписанте.
*/
export interface ISignerInfo {
/**
* Фамилия.
*/
familyName: string;
/**
* Имя.
*/
firstName: string;
/**
* Отчество.
*/
middleName: string;
/**
* Тип пользователя.
*/
type: UserType;
/**
* Идентификатор пользователя.
*/
uaaId: string;
}
/**
* Интерфейс сервиса отправки подписи.
*/
export interface IEcoSendService {
/**
* Отправить документ.
*
* @param id Отправить документ.
*/
send(id: string): Promise<IServerResp<any>>;
/**
* Отправить документ.
*
* @param docs Документы на отправку.
*/
sendAll?(docs: any): Promise<Array<IServerResp<any>>>;
}
/**
* Интерфейс сервиса подписи.
*/
export interface IEcoSignService {
/**
* Метод сохранения данных на сервере.
*
* @param data Сохраняемые данные.
*/
sign(data: ISignRequestData): Promise<IBaseEntity>;
/**
* Метод сохранения данных на сервере.
*
* @param data Сохраняемые данные.
*/
signAll?(data: ISignRequestData[]): Promise<IBaseEntity[]>;
/**
* Получение подписываемых данных.
*
* @param id Идентификатор документа.
*/
getSignData?(id: string): Promise<IUniversalSignResponse>;
/**
* Получение подписываемых данных v2.1.
*
* @param id Идентификатор документа.
*/
getSignDataV2?(id: string): Promise<IUniversalSignV2Response>;
/**
* Получение подписываемых данных.
*
* @param ids Идентификаторы документа.
*/
getAllSignData?(ids: string[]): Promise<IUniversalSignV2Response>;
/**
* Метод получения данных статистики.
*
* @param docIds Идентификаторы документов.
* @param certificateId Идентификатор сертификата.
*/
getStatistics?(docIds: string[], certificateId: string): Promise<any>;
}
/**
* Результат проверки пин-кода.
*/
export interface CheckPinCodeResponse {
/**
* Оставшиеся попытки.
*/
remaining: number;
/**
* Код результата.
*/
code: PIN_CODE_CHECK;
}
+14
View File
@@ -0,0 +1,14 @@
export interface IUserName {
/**
* Фамилия.
*/
familyName: string;
/**
* Имя.
*/
firstName: string;
/**
* Отчество.
*/
middleName: string | null | undefined; // может быть null, если не заполнено (присылает бек)
}
@@ -0,0 +1,12 @@
import { CRYPTO_MODULE_SIGN_TYPE } from '../../stream-constants';
const olkCryptoModule = {
installationCheck: () => Promise.resolve({ data: '1.1.1' }),
init: () => Promise.resolve({ keySessID: 'keySessID' }),
batchSign: (_, signData: string[]) => Promise.resolve(signData.map(item => ({ data: item, signType: CRYPTO_MODULE_SIGN_TYPE.CMS }))),
verifySign: () => ({ data: {}, status: 'VALID', error: { code: 0 } }),
};
module.exports = {
olkCryptoModule,
};
@@ -0,0 +1 @@
export * from './settings';
@@ -0,0 +1,8 @@
import { getAppSettings } from '../settings';
const SETTINGS_URL = '/api/settings-bank/settings/system';
const PUBLIC_SETTINGS_URL = '/api/settings-bank/settings/public/system';
/** Получение экземпляра сервиса загрузки настроек. */
export const appSettingsService = getAppSettings(SETTINGS_URL);
export const appPublicSettingsService = getAppSettings(PUBLIC_SETTINGS_URL);
@@ -0,0 +1 @@
export * from './settings';
@@ -0,0 +1,8 @@
import { getAppSettings } from '../settings';
const SETTINGS_URL = '/api/settings-client/settings/system';
const PUBLIC_SETTINGS_URL = '/api/settings-client/settings/public/system';
/** Получение экземпляра сервиса загрузки настроек. */
export const appSettingsService = getAppSettings(SETTINGS_URL);
export const appPublicSettingsService = getAppSettings(PUBLIC_SETTINGS_URL);
@@ -0,0 +1,252 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { to } from '@msb/http';
import { settingsStore } from '@msb/http/settings-client';
import type { AxiosRequestConfig, AxiosInstance, AxiosError, AxiosResponse } from 'axios';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import axios from 'axios';
import type { Installer } from '../interfaces';
import { SETTING_CODE } from '../interfaces';
import type {
IGPBCryptoModuleCheck,
IGPBCryptoModuleCheckResponse,
IGPBCryptoModuleInitkey,
IGPBCryptoModuleResultInitKey,
IGPBCryptoModuleSign,
IGPBCryptoModuleSignResult,
} from '../interfaces/crypto-module';
import { CRYPTO_PLUGIN_TYPE } from '../stream-constants';
import { PLUGIN_TYPE_STORAGE_KEY } from '../stream-constants/common';
import {
CODE_MAP,
CRYPTO_PLUGIN_BY_DEFAULT,
PLUGIN_NETWORK_ERROR_MESSAGE,
VERSION_ERROR_MESSAGE,
CRYPTO_PLUGIN_AUTO_DETECTION,
CRYPTO_PLUGINS_FEATURE,
} from '../stream-constants/crypto';
import { ERROR } from '../stream-constants/error';
import { canUseCryptoPlugin } from '../utils/crypto-module';
import { checkCdnUrlAvailability, getBssPluginVersion, getCmType, getSftPluginVersion } from '../utils/olk';
import { captureCryptoModuleException } from '../utils/sentry';
/**
* Плагин под которым пользователем должен авторизоваться в системе.
*/
let cryptoPluginType: CRYPTO_PLUGIN_TYPE = CRYPTO_PLUGIN_TYPE.SFT;
let canUseCrypto = false;
const instance: AxiosInstance = axios.create({ baseURL: '' });
/**
* Перехват и обработка ошибки.
*/
const handleError = (err: AxiosError<any>) => {
captureCryptoModuleException(err);
let code = 0;
if (err.response) {
if (err.response.data?.code) {
code = CODE_MAP[err.response.data.code];
}
} else {
switch (err.message) {
case PLUGIN_NETWORK_ERROR_MESSAGE:
code = ERROR.CRYPTO_NOT_FOUND;
break;
case VERSION_ERROR_MESSAGE:
code = ERROR.CRYPTO_MODULE_VERSION_INVALID;
break;
default:
break;
}
}
return Promise.reject({
response: {
data: {
errorInfo: {
code,
},
data: (err.response || {}).data,
},
},
});
};
/**
* Перехват и обработка запроса.
*/
const handleRequest = (res: AxiosResponse) => {
const { error, ...otherData } = res.data ?? {};
if (Object.keys(otherData).length === 0) {
return handleError({
response: {
data: error,
},
} as unknown as AxiosError);
}
return {
data: {
error,
data: otherData.data ?? otherData.certificates,
},
};
};
// @ts-ignore
instance.interceptors.response.use(handleRequest, handleError);
instance.interceptors.request.use(async axiosRequest => {
const config = settingsStore.systemSettings.get(SETTING_CODE.SETTINGS) as Record<string, any>;
if (!config) {
return axiosRequest;
}
const plugin = (sessionStorage.getItem(PLUGIN_TYPE_STORAGE_KEY) as CRYPTO_PLUGIN_TYPE) ?? CRYPTO_PLUGIN_TYPE.SFT;
cryptoPluginType = plugin;
if (canUseCrypto) {
return axiosRequest;
}
const plugins = config[CRYPTO_PLUGINS_FEATURE];
const checkAvailableCryptoModules = config[CRYPTO_PLUGIN_AUTO_DETECTION];
const availablePlugins = [
{ ...plugins[plugin], type: plugin },
...(checkAvailableCryptoModules
? Object.entries(plugins)
.filter(([pluginType]) => plugin !== pluginType)
.map(([type, userPlugin]) => ({
// @ts-ignore
...userPlugin,
type,
}))
: []),
];
const pluginsResult = await Promise.all(
availablePlugins.map(async ({ url, minVersion, type }) => {
let availableRequest: Promise<string>;
switch (type) {
case CRYPTO_PLUGIN_TYPE.BSS:
availableRequest = getBssPluginVersion();
break;
case CRYPTO_PLUGIN_TYPE.SFT:
availableRequest = getSftPluginVersion();
break;
default:
break;
}
const [res, err] = await to(availableRequest!);
return {
url,
type,
version: res,
canUse: canUseCryptoPlugin(res, minVersion),
err,
};
})
);
const firstPluginResult = pluginsResult[0];
// eslint-disable-next-line require-atomic-updates
instance.defaults.baseURL = firstPluginResult.url;
axiosRequest.baseURL = firstPluginResult.url;
const workingPlugin = pluginsResult.find(axiosPlugin => axiosPlugin.canUse && !axiosPlugin.err);
// eslint-disable-next-line require-atomic-updates
canUseCrypto = Boolean(workingPlugin);
if (!workingPlugin) {
const usePlugin = pluginsResult.find(axiosPlugin => !axiosPlugin.err);
if (usePlugin) {
cryptoPluginType = usePlugin.type as CRYPTO_PLUGIN_TYPE;
throw new Error(VERSION_ERROR_MESSAGE);
} else {
throw new Error(PLUGIN_NETWORK_ERROR_MESSAGE);
}
}
return axiosRequest;
});
/**
* Отдает тип криптоплагина.
*/
export const getCryptoPluginType = () => cryptoPluginType;
/**
* Скачать криптоплагин.
*/
export const downloadCryptoPlugin = async (type?: keyof Installer) => {
const config = settingsStore.systemSettings.get(SETTING_CODE.SETTINGS) as Record<string, any>;
if (!config) {
return;
}
const cryptoPluginAutoDetection = config[CRYPTO_PLUGIN_AUTO_DETECTION];
const cryptoPluginByDefault = config[CRYPTO_PLUGIN_BY_DEFAULT];
const plugin = cryptoPluginAutoDetection && cryptoPluginByDefault in CRYPTO_PLUGIN_TYPE ? cryptoPluginByDefault : cryptoPluginType;
const downloadType = getCmType(type);
const { cdn, proxy } = config.cryptoPluginsInstaller[plugin];
let downloadUrl = '';
try {
await checkCdnUrlAvailability(cdn[downloadType]);
downloadUrl = cdn[downloadType];
} catch (e) {
downloadUrl = proxy[downloadType];
}
window.open(downloadUrl);
};
export const request = <T = any>(params: AxiosRequestConfig): Promise<T> => instance(params).then(r => r.data.data);
const SERT_API_URL = '/api/v1';
/**
* Сервис запросов к КМ.
*/
export const cryptoModuleService = {
version: () =>
request<string>({
url: `${SERT_API_URL}/version`,
method: 'GET',
}),
getInitkey: (data: IGPBCryptoModuleInitkey) =>
request<IGPBCryptoModuleResultInitKey>({
url: `${SERT_API_URL}/initkey`,
method: 'POST',
data,
}),
sign: (data: IGPBCryptoModuleSign) =>
request<IGPBCryptoModuleSignResult[]>({
url: `${SERT_API_URL}/sign`,
method: 'POST',
data,
}),
check: (data: IGPBCryptoModuleCheck): Promise<IGPBCryptoModuleCheckResponse> =>
instance({
url: `${SERT_API_URL}/check`,
method: 'POST',
data,
}).then(resp => resp.data),
};
+3
View File
@@ -0,0 +1,3 @@
export * from './crypto-module';
export * from './olk';
export * from './settings';
+67
View File
@@ -0,0 +1,67 @@
import type { ICryptoModule, IVerifySignResult, ISignParams } from '../interfaces';
import { CRYPTO_MODULE_SIGN_TYPE, SESSION_ID_NOT_FOUND } from '../stream-constants';
import { getCertificate, getCertificateLocation, getSessionId, initSession, setSessionId, getCheckStatus, retry } from '../utils';
import { cryptoModuleService } from './crypto-module';
const beforeRetry = async () => {
setSessionId('');
await initSession({ containerNameLocation: getCertificateLocation(), certData: getCertificate() });
};
const retryCheck = (code: number) => code === SESSION_ID_NOT_FOUND;
/**
* Сервис подписи КМ.
*/
export const olkCryptoModule: ICryptoModule = {
init: initSession,
batchSign: (
_,
signData,
sessId?: string,
detached?: boolean,
calculatedHash?: boolean,
signType: ISignParams['signType'] = CRYPTO_MODULE_SIGN_TYPE.CMS
) =>
retry(
() =>
cryptoModuleService
.sign({ data: signData, keySessID: sessId || getSessionId(), signType, detached, calculatedHash })
.then(result => result.map(({ data }) => data)),
retryCheck,
beforeRetry
),
verifySign: async ({
signature,
data,
certificate,
certificatesCA = [],
crls = [],
providerName,
calculatedHash,
}): Promise<IVerifySignResult> => {
const providerInfo = providerName
? {
providerName,
}
: undefined;
const resp = await cryptoModuleService.check({
certificate,
data,
signedData: [{ data: signature, signType: CRYPTO_MODULE_SIGN_TYPE.CMS }],
certificatesCA,
CRLs: crls,
providerInfo,
calculatedHash,
});
return {
data: resp.data,
status: getCheckStatus(resp.error),
error: resp.error,
};
},
installationCheck: cryptoModuleService.version,
};
+28
View File
@@ -0,0 +1,28 @@
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import type { AxiosResponse } from '@msb/http';
import { network } from '@msb/http';
import type { IDataResponse, ISetting } from '../interfaces';
import { getHeaders } from '../utils/headers';
export const appSettingsResponse = <T>({ data }: AxiosResponse<IDataResponse<T>>) => {
if (data.message) {
throw data.message;
}
return data.data;
};
/**
* Сервис получения настроек.
*/
export const getAppSettings = (url: string) => ({
getByCode: (code: string) =>
network
.client<IDataResponse<ISetting>>({
method: 'GET',
url: `${url}/${code}`,
headers: getHeaders(),
})
.then(appSettingsResponse),
});
@@ -0,0 +1,9 @@
/**
* Namespace локали.
*/
export const LOCALE_NAME = 'crypto';
/**
* Ключ sessionStorage для вычитки типа используемого крипто плагина.
*/
export const PLUGIN_TYPE_STORAGE_KEY = 'PLUGIN_TYPE_STORAGE_KEY';
@@ -0,0 +1,76 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { ERROR } from './error';
/**
* Статус проверки подписи.
*/
export enum CHECK_STATUS {
/**
* Подпись верна.
*/
VALID = 'VALID',
/**
* Подись не верна.
*/
INVALID = 'INVALID',
/**
* Проверка не выполнена.
*/
NOT_CHECKED = 'NOT_CHECKED',
}
/**
* Коды ошибок КМ.
*/
export enum CRYPTO_ERROR_CODE {
NOT_FOUND = 1,
METHOD_NOT_ALLOWED = 2,
FORBIDDEN = 3,
BAD_REQUEST = 4,
UNSUPPORTED_MEDIA_TYPE = 5,
UNAUTHORIZED = 6,
LOCKED = 7,
INTERNAL_SERVER_ERROR = 99,
USER_IS_DENIED_ACCESS = 100,
INVALID_AUTHORIZATION_INFO = 101,
KEY_CONTAINER_NOT_FOUND = 102,
}
/**
* Маппер ошибок КМ.
*/
export const CODE_MAP: Record<string, ERROR> = {
[ERROR.NO_CONNECTION]: ERROR.CRYPTO_NOT_FOUND,
[CRYPTO_ERROR_CODE.BAD_REQUEST]: ERROR.CRYPTO_INCORRECT_AUTHORIZATION_INFO,
[CRYPTO_ERROR_CODE.USER_IS_DENIED_ACCESS]: ERROR.CRYPTO_UNAUTHORIZE,
[CRYPTO_ERROR_CODE.UNAUTHORIZED]: ERROR.UNAUTHORIZED,
[CRYPTO_ERROR_CODE.FORBIDDEN]: ERROR.FORBIDDEN,
[CRYPTO_ERROR_CODE.NOT_FOUND]: ERROR.NOT_FOUND,
[CRYPTO_ERROR_CODE.METHOD_NOT_ALLOWED]: ERROR.METHOD_NOT_ALLOWED,
[CRYPTO_ERROR_CODE.UNSUPPORTED_MEDIA_TYPE]: ERROR.UNSUPPORTED_MEDIA_TYPE,
[CRYPTO_ERROR_CODE.LOCKED]: ERROR.LOCKED,
[CRYPTO_ERROR_CODE.INTERNAL_SERVER_ERROR]: ERROR.CRYPTO_INTERNAL_SERVER_ERROR,
[ERROR.VALIDATION_SERVER_CONTROLS_ERROR]: ERROR.CRYPTO_CERTS_NOT_FOUND,
[CRYPTO_ERROR_CODE.KEY_CONTAINER_NOT_FOUND]: ERROR.CERTIFICATES_NOT_FOUND,
};
/**
* Сообщения об ошибках КМ.
*/
export const PLUGIN_NETWORK_ERROR_MESSAGE = 'Network Error';
export const VERSION_ERROR_MESSAGE = 'Plugin Version Error';
/**
* Наименование параметра информации об используемых плагинах.
*/
export const CRYPTO_PLUGINS_FEATURE = 'cryptoPlugins';
/**
* Наименование параметра автоопределения плагина.
*/
export const CRYPTO_PLUGIN_AUTO_DETECTION = 'cryptoPluginAutoDetection';
/**
* Определяет какой криптоплагин будет предложен пользователю для установки
* при нажатии на кнопку «Установить» в случае, когда плагин не обнаружен.
*/
export const CRYPTO_PLUGIN_BY_DEFAULT = 'cryptoPluginByDefault';
@@ -0,0 +1,43 @@
/**
* Вид ЭП.
*/
export enum SIGN_KIND {
/**
* Локальная ЭП.
*/
LOCAL = 'LOCAL',
/**
* Облачная НЭП.
*/
CLOUD_SIGN = 'CLOUD_SIGN',
/**
* НЭП.
*/
NOT_QUALIFIED_SIGN = 'NOT_QUALIFIED_SIGN',
/**
* КЭП.
*/
QUALIFIED_SIGN = 'QUALIFIED_SIGN',
/**
* Простая ЭП.
*/
SIMPLE_SIGN = 'SIMPLE_SIGN',
}
/**
* Метод вторичной аутентификации.
*/
export enum PROFILE_AUTH_METHOD {
/**
* Неизвестен.
*/
UNKNOWN = 'UNKNOWN',
/**
* Мобильное приложение myDss 1.0.
*/
MOBILE = 'MOBILE',
/**
* Мобильное приложение DSS SDK.
*/
MY_DSS = 'MY_DSS',
}
@@ -0,0 +1,91 @@
export enum ERROR {
// CUSTOM
NO_RESPONSE = 0,
NO_CONNECTION = 1,
// Аутентификация
ONETIME_PASSWORD_EXPIRED = 100, // Время действия одноразового пароля истекло
INVALID_VERIFICATION_CODE = 101, // Неверное значение кода подтверждения
ACCOUNT_NOT_FOUND = 102, // Модальное окно или текст перед кнопкой продолжить
CONFIRMATION_ATTEMPTS_EXHAUSTED = 103, // Исчерпано количество попыток ввода кода подтверждения
INCORRECT_LOGIN_PASSWORD = 104, // Введены неверно логин или пароль
PASSWORD_ATTEMPTS_EXHAUSTED = 105, // Исчерпано количество попыток ввода пароля
MFA_TOTP_USER_NOT_FOUND = 189, // Ненайдены totp настройки для пользователя
// Многофакторная аутентификация
/**
* Неправильный токен.
*/
MFA_INVALID_TOKEN_VALUE = 190,
/**
* Не истек срок действия смс-кода.
*/
SMS_TIMEOUT_NOT_EXPIRED = 191,
MFA_INVALID_CREDENTIALS = 192, // неправильные креды смс код
LOGIN_REQUIRED = 193, // требуется логин
APPROVE_REQUESTED = 194, // требуется аппрув
USER_BLOCKED = 195, // юзер заблокирован
INVALID_CREDENTIALS = 196, // неправильные креды (логин/пароль)
MFA_TOKEN_ATTEMPS_EXHAUSTED = 197, // Исчерпано количество попыток ввода кода подтверждения
MFA_TOKEN_EXPIRED = 198, // истекло время жизни токена авторизации
MFA_REQUIRED = 199, // требуется многофакторная аутентификация
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404, // запись не найдена
METHOD_NOT_ALLOWED = 405, // Метод API отсутствует
UNSUPPORTED_MEDIA_TYPE = 415,
LOCKED = 420,
OPTIMISTIC_LOCK = 423,
FILE_MALWARE = 600, // файл не прошел проверку антивирусом
IN_STOP_LIST = 644, // Клиент в стоп-листе
// Ошибки крипты
CRYPTO_NOT_FOUND = 800, // крипто-модуль не установлен
CRYPTO_UNAUTHORIZE = 801, // отказано в доступе, неправильные ключи, неправильный пароль
CRYPTO_CERTS_NOT_FOUND = 802, // отсутствуют сертификаты
CRYPTO_INCORRECT_AUTHORIZATION_INFO = 803, // Передана не корректная информация для авторизации
CRYPTO_SIGN_NOT_CORRECT = 804, // Подпись не верна
CRYPTO_VERIFYING_CERTIFICATE_CHAIN = 805, // Ошибка при проверке цепочки сертификатов
CRYPTO_INVALID_SIGN_CERTIFICATE = 806, // Неверный сертификат подписи
CRYPTO_MODULE_VERSION_INVALID = 807, // Некорректная версия криптомодуля
CRYPTO_INTERNAL_SERVER_ERROR = 899,
// Регистрация
UNEXPECTED_SYSTEM_ERROR = 500, // Непредвиденная системная ошибка
LOGIN_OR_EMAIL_EXIST = 901, // Введен логин или email, который есть в системе
EMAIL_EXIST = 902, // Введен email, который есть в системе
PREREGISTERED_USER_NOT_FOUND = 903, // Предварительно зарегистрированный пользователь для указанного подтверждения не найден
LOGIN_NOT_FOUND = 904, // Пользователь с указанным логином не найден
PASS_REC_USER_BLOCKED = 905, // юзер заблокирован приходит (при востановлении пароля)
USER_NOT_CLIENT = 906, // Пользователь не является клиентским пользователем
INCORRECT_PASSWORD = 907, // Некорректный пароль
PASSWORD_UNSAFE = 908, // Введенный пароль не отвечает требованиям парольной политики
PASSWORD_USE_ANOTHER_USER = 909, // Введенный пароль уже используется другим пользователем
EMAIL_DELIVERY_ERROR = 910, // Ошибка при доставке электронной почты
CAPTCHA_ERROR = 911, // Неверно введена последовательность символов с капчи
INVALID_PHONE_NUMBER = 912, // Некорректный номер телефона
LOGOUT_DUE_TO_EXEEDED_TRYES = 913, // Превышено количество попыток ввода кода подтверждения / пароля. Ваша сессия завершена.
INVALID_EMAIL = 915, // Некорректный email
CHANGE_DETAILS_ERROR = 920, // ошибка изменения данных пользователя
PHONE_CHANGE_ON_FOREIGN_NOT_ALLOWED = 922, // запрещена смена телефона с российского на иностранный
PHONE_CHANGE_ON_RUSSIAN_NOT_ALLOWED = 923, // запрещена смена телефона с иностранного на российский
ONETIME_PASSWORD_SEND_TIMEOUT = 950, // Таймаут отправки кодов подтверждения
VERIFICATION_CODESEND_FAILED = 951, // Ошибка проверки кода подтверждения
ONETIME_PASSWORD_EXPIRED_REG = 952, // Время действия одноразового пароля истекло (при регистрации)
CONFIRMATION_ATTEMPTS_EXHAUSTED_REG = 953, // Количество попыток ввода кода подтверждения истекло
FAILED_SEND_CODE = 954, // Не удалось выслать код на телефон
CRYPTOMODULE_NOT_INSTALLED = 955, // Криптомодуль не установлен
CERTIFICATES_NOT_FOUND = 956, // Нет доступных сертификатов
/**
* Превышен лимит отправленных SMS.
*/
NUMBER_OF_TRIES_EXCEEDED_SEND_SMS = 940,
CERTIFICATES_NOT_RIGHT_SIGN = 957, // Недостаточно прав для подписи документа, поскольку Вы не являетесь ЕИО (единоличным исполнительным органом)
VALIDATION_SERVER_CONTROLS_ERROR = 999, // Проверка серверными контролями не пройдена
}
@@ -0,0 +1,9 @@
export * from './error';
export * from './dst';
export * from './crypto';
export * from './olk';
export * from './sign';
export * from './common';
export * from './linux';
export * from './urls';
export * from './user';
@@ -0,0 +1,4 @@
/**
* Тип платформы Linux для скачивания КМ.
*/
export const LINUX_TYPES = ['astra', 'alt', 'red'];
@@ -0,0 +1,34 @@
/**
* Тип контейнера.
*/
export enum CONTAINER_TYPE {
SCOM = 'ContainerSCOMNative',
DBOBSS = 'ContainerDboBss',
}
/**
* Тип криптоплагина.
*/
export enum CRYPTO_PLUGIN_TYPE {
BSS = 'BSS',
SFT = 'SFT',
}
/**
* Тип подписи.
*/
export enum CRYPTO_MODULE_SIGN_TYPE {
/**
* Массив байт.
*/
PLAIN = 'PLAIN',
// прочие форматы
SFT = 'SFT',
CMS = 'CMS',
CADES_BES = 'CAdES_BES',
CADES_T = 'CAdES_T',
CADES_X_LONG_TYPE_1 = 'CAdES_X_Long_Type_1',
CADES_A = 'CAdES_A',
}
export const SESSION_ID_NOT_FOUND = 6;
@@ -0,0 +1,17 @@
/**
* Код проверки пин-кода.
*/
export enum PIN_CODE_CHECK {
/**
* Пароль верный.
*/
VALID_PIN_CODE = 'VALID_PIN_CODE',
/**
* Пароль неверный.
*/
INVALID_PIN_CODE = 'INVALID_PIN_CODE',
/**
* Ключ заблокирован.
*/
DST_PRIVATE_KEY_BLOCKED = 'DST_PRIVATE_KEY_BLOCKED',
}
@@ -0,0 +1,13 @@
/**
* Url для перехода.
*/
export const STREAM_URLS = {
/**
* Профиль пользователя.
*/
USER: '/user',
/**
* Страница помощи.
*/
HELP: '/help',
};
@@ -0,0 +1,19 @@
import { localization } from '@msb/localization';
import { LOCALE_NAME } from './common';
const t = localization.translate(LOCALE_NAME);
/**
* Локализованные типы пользователя.
*/
export const USER_TYPE_LABELS = {
get BANK() {
return t('sign.modal.fractalVerifySignModal.txt.roleBank');
},
get CLIENT() {
return t('sign.modal.fractalVerifySignModal.txt.roleClient');
},
get TECHNICAL() {
return t('sign.modal.fractalVerifySignModal.txt.roleSystem');
},
};
+4
View File
@@ -0,0 +1,4 @@
declare module '*.svg' {
const value: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
export default value;
}
@@ -0,0 +1,7 @@
module.exports = {
checkInterval: (fn: (...args: any) => Promise<any>) => {
const checkFn = (...args) => new Promise(resolve => fn(...args).then(res => resolve(res)));
return [checkFn, null];
},
};
@@ -0,0 +1,5 @@
import { mockConfig } from '../../helpers/config';
module.exports = {
getSettingConfigValue: (key: string) => mockConfig[key],
};
@@ -0,0 +1,3 @@
module.exports = {
t: (key: string) => key,
};
@@ -0,0 +1,34 @@
import { getCmType } from '../olk';
import * as factory from '../os';
jest.mock('../config', () => {
const module = jest.requireActual('../config');
return {
__esModule: true,
...module,
getSettingConfigValue: value => value,
};
});
describe('utils', () => {
it('тип дистрибутива КМ = win', () => {
jest.spyOn(factory, 'isWin').mockReturnValueOnce(true);
expect(getCmType()).toBe('win');
});
it('тип дистрибутива КМ = mac', () => {
jest.spyOn(factory, 'isMac').mockReturnValueOnce(true);
expect(getCmType()).toBe('mac');
});
it('тип дистрибутива КМ = nix', () => {
expect(getCmType()).toBe('nix');
});
it('тип дистрибутива КМ равен переданному значению', () => {
expect(getCmType('alt')).toBe('alt');
});
});
+36
View File
@@ -0,0 +1,36 @@
/**
* Метод ожидающий завершения функции.
*/
export const checkInterval = <T, TArgs extends any[]>(
fn: (...args: TArgs) => Promise<T>,
interval: number,
maxCheckTime: number
): [(...args: TArgs) => Promise<T>, () => void] => {
const checkEnd = maxCheckTime * 60 * 1000 + Date.now();
let intervalId: ReturnType<typeof setInterval>;
const cancel = (): void => {
if (intervalId) {
clearInterval(intervalId);
}
};
const check = (...args: TArgs): Promise<T> =>
new Promise<T>((resolve, reject) => {
intervalId = setInterval(() => {
if (Date.now() > checkEnd) {
clearInterval(intervalId);
reject();
}
void fn(...args).then(res => {
if (res) {
clearInterval(intervalId);
resolve(res);
}
});
}, interval * 1000);
});
return [check, cancel];
};
+12
View File
@@ -0,0 +1,12 @@
let certificate = '';
let certificateLocation = '';
export const getCertificate = () => certificate;
export const setCertificate = (newCertificate: string) => {
certificate = newCertificate;
};
export const getCertificateLocation = () => certificateLocation;
export const setCertificateLocation = (newLocation: string) => {
certificateLocation = newLocation;
};
@@ -0,0 +1,24 @@
import { gte, coerce } from 'semver';
/**
* Проверка используемой версии плагина.
*
* @param version Версия плагина.
* @param minVersion Минимальная версия плагина.
*/
export const canUseCryptoPlugin = (version: string | null, minVersion?: string) => {
const min = coerce(minVersion);
const current = coerce(version);
if (!min) {
return true;
}
if (!current) {
return false;
}
const comparerResult = gte(current, min);
return comparerResult;
};
+6
View File
@@ -0,0 +1,6 @@
/**
* Получить человекочитаемое время мм:cc.
*
* @param seconds Секунды.
*/
export const getTimeLeft = (seconds: number) => new Date(seconds * 1000).toISOString().substr(14, 5);
+21
View File
@@ -0,0 +1,21 @@
type PlainObject = Record<string, any>;
const isObject = (item: unknown): item is PlainObject => item !== undefined && typeof item === 'object' && !Array.isArray(item);
/**
* Осуществляет рекурсивный мерж полей двух объектов. Аналог метода lodash.merge.
*
* @param target Целевой объект.
* @param source Исходный объект.
* @returns Результирующий объект.
*/
export const deepMerge = <T extends PlainObject, U extends PlainObject>(target: T, source: U): T & U => {
const result = { ...target, ...source };
const keys: Array<keyof T & keyof U> = Object.keys(result);
for (const key of keys) {
result[key] = isObject(target[key]) && isObject(source[key]) ? deepMerge(target[key], source[key]) : structuredClone(result[key]);
}
return result;
};
+27
View File
@@ -0,0 +1,27 @@
/**
* Функция создания генератора.
*/
export const getInfiniteGen = () => {
const runStatus = {
stopped: false,
};
const run = function* run() {
while (true) {
if (runStatus.stopped) {
break;
}
yield true;
}
};
const stop = () => {
runStatus.stopped = true;
};
return {
run,
stop,
};
};
+10
View File
@@ -0,0 +1,10 @@
import { TOKEN_KEY, XSRF_HEADER_NAME } from '@msb/http/network/constants';
/**
* Метод возвращающий хедеры для запросов.
*/
export const getHeaders = () => ({
'x-xsrf-token': sessionStorage.getItem(XSRF_HEADER_NAME),
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)!}`,
'Locale-code': 'ru',
});
+16
View File
@@ -0,0 +1,16 @@
export * from './certificate';
export * from './olk';
export * from './session';
export * from './mobile';
export * from './is-linux';
export * from './crypto-module';
export * from './deep-merge';
export * from './date-time';
export * from './async';
export * from './generator';
export * from './request';
export * from './user';
export * from './scope';
export * from './request';
export * from './headers';
export * from './generator';
+17
View File
@@ -0,0 +1,17 @@
import { detect } from 'detect-browser';
const getOs = () => detect()?.os?.toLowerCase() || '';
/**
* Проверка платформы пользователя.
*
* @returns True, если ОС семейства Linux, false в остальных случаях.
*/
export const isLinux = () => getOs().includes('linux');
/**
* Проверка платформы пользователя.
*
* @returns True, если ОС Windows, false в остальных случаях.
*/
export const isWin = () => getOs().includes('windows');
+106
View File
@@ -0,0 +1,106 @@
import type { IosApp, MobileApp, AndroidApp } from '../interfaces';
export const isAndroid = () => /android/i.test(navigator.userAgent);
export const isIos = () => {
// Свойство platform помечено как устаревшее, однако свойства userAgentData нет в интерфейса Navigator
// eslint-disable-next-line compat/compat
const platform = navigator?.platform;
// Свойство maxTouchPoints поддерживается не всеми браузерами
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/maxTouchPoints
// eslint-disable-next-line compat/compat
return /iphone|ipad|ipod/i.test(navigator.userAgent) || (platform === 'MacIntel' && navigator.maxTouchPoints > 0) || platform === 'iPad';
};
/**
* Проверка устройства пользователя.
*
* @returns True, если мобильное устройство, false если desktop.
*/
export const isMobile = () => isAndroid() || isIos();
/**
* Открытие App Store.
*/
const installIosApp = (app: IosApp) => {
window.location.href = `https://apps.apple.com/app/${app.name}/id${app.id}`;
};
/**
* Открытие Play Market.
*/
const installAndroidApp = (app: AndroidApp) => {
window.location.href = `market://details?id=${app.id}`;
};
/**
* Открытие соответствующего магазина приложений.
*/
const installApp = (app: MobileApp) => {
if (isAndroid()) {
installAndroidApp(app.android);
} else if (isIos()) {
installIosApp(app.ios);
}
};
/**
* Метод открытия мобильного приложения.
*/
const openApp = (app: MobileApp) => {
if (isAndroid()) {
window.location.href = `${app.android.name}://`;
} else if (isIos()) {
window.location.href = `${app.ios.name}://`;
}
};
/**
* Метод открывающий приложение, если не установлено, открывается store.
*/
export const getMobileApp = (app: MobileApp) => {
let wasOpenedApp = false;
let timerId: NodeJS.Timeout | number | undefined;
const clearTimer = () => {
if (timerId) {
clearTimeout(timerId);
}
};
const visibilityHandler = () => {
if (document.hidden) {
wasOpenedApp = true;
}
};
const install = () => {
clearTimer();
timerId = setTimeout(() => {
if (!wasOpenedApp) {
installApp(app);
wasOpenedApp = true;
document.removeEventListener('visibilitychange', visibilityHandler);
window.removeEventListener('focus', install);
window.removeEventListener('blur', clearTimer);
}
}, 1500);
};
openApp(app);
if (isAndroid()) {
install();
}
document.addEventListener('visibilitychange', visibilityHandler);
window.addEventListener('focus', install);
window.addEventListener('blur', clearTimer);
};
+237
View File
@@ -0,0 +1,237 @@
import React from 'react';
import { Text } from '@fractal-ui/styling';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { XSRF_HEADER_NAME } from '@msb/http/network/constants';
import { settingsStore } from '@msb/http/settings-client';
import { localization } from '@msb/localization';
import axios from 'axios';
// import { getConfig } from '@platform/core';
import { DiagnosticLink, HowToLink } from '../components';
// import type { ISettingsConfig } from '../interfaces';
import { SETTING_CODE } from '../interfaces';
import type { IError } from '../interfaces/crypto-module';
import type { ICryptoError, ICryptoModuleErrorResponse } from '../interfaces/olk';
import { VERIFY_SIGN_ERROR } from '../interfaces/olk';
import { LOCALE_NAME } from '../stream-constants';
import { CHECK_STATUS, CODE_MAP, CRYPTO_PLUGINS_FEATURE } from '../stream-constants/crypto';
import { isMac, isWin } from './os';
const VERSION_REQUEST_TIMEOUT = 3 * 1000;
const CDN_REQUEST_TIMEOUT = 3 * 1000;
const t = localization.translate(LOCALE_NAME);
export const getValues = (obj: Record<string, any>) => Object.keys(obj).map(key => obj[key]);
/**
* Получить версию бсс плагина, установленного на компьютере пользователя.
*
* @throws Axios error.
*/
export const getBssPluginVersion = async () => {
const config = settingsStore.systemSettings.get(SETTING_CODE.SETTINGS) as Record<string, any>;
if (!config) {
return;
}
const bss = config[CRYPTO_PLUGINS_FEATURE].BSS;
try {
const result = await axios.post(
bss.url,
{
Plugin: 'AXTools',
Method: 'GetProperty',
Params: [{ Version: '' }],
},
{
timeout: VERSION_REQUEST_TIMEOUT,
}
);
return result.data.Result;
} catch (e) {
throw new Error(e as string);
}
};
/**
* Получить версию сфт плагина, установленного на компьютере пользователя.
*
* @throws Axios error.
*/
export const getSftPluginVersion = async () => {
const config = settingsStore.systemSettings.get(SETTING_CODE.SETTINGS) as Record<string, any>;
if (!config) {
return;
}
const sft = config[CRYPTO_PLUGINS_FEATURE].SFT;
try {
const result = await axios({
url: `${sft.url}/api/v1/version/service`,
timeout: VERSION_REQUEST_TIMEOUT,
});
return result.data.data;
} catch (e) {
throw new Error(e as string);
}
};
/**
* Проверить доступность cdn.
*
* @param url URL адрес cdn.
*/
export const checkCdnUrlAvailability = (url: string) =>
axios({
method: 'HEAD',
url,
timeout: CDN_REQUEST_TIMEOUT,
headers: {
'x-xsrf-token': sessionStorage.get(XSRF_HEADER_NAME),
'Locale-code': 'ru',
},
});
/**
* Повтор выполнения функции в зависимости от условия.
*
* @param fn Выполняемая функция.
* @param retryCheck Функция проверки.
* @param beforeRetry Код выполняемый до повтора выполнения функции.
*
* @throws Error ошибка выполнения.
*/
export const retry = async <T, F extends () => Promise<T>>(
fn: F,
retryCheck: (error: ICryptoError['code']) => boolean,
beforeRetry: () => Promise<void>
) => {
try {
return await fn();
} catch (e) {
if (retryCheck((e as ICryptoModuleErrorResponse<ICryptoError>).response.data.data.code)) {
await beforeRetry();
return fn();
}
throw e;
}
};
/**
* Получение статуса проверки подписи.
*/
export const getCheckStatus = (error?: IError): CHECK_STATUS => {
if (!error || error.code === 0) {
return CHECK_STATUS.VALID;
}
if (getValues(VERIFY_SIGN_ERROR).includes(error.code)) {
return CHECK_STATUS.INVALID;
}
return CHECK_STATUS.NOT_CHECKED;
};
/**
* Получение хеша ссылки на ошибку.
*/
const getLinkHash = (error: IError) => (error?.link ?? '').split('#')[1];
/**
* Проверка хэша ошибки.
*
* Если функция вернула true, то в качестве рекомендации по устраниению
* будет отображаться "Проведите диагностику".
*/
const isCm1Error = (error: IError) => {
const hashLink = getLinkHash(error);
if (!hashLink) {
return false;
}
return hashLink.startsWith('cm1_') || hashLink === 'cm1';
};
/**
* Получение ключа локали для рекомендации по устранению.
*/
const getKey = (error: IError) => {
if (isCm1Error(error)) {
return 'sign.txt.diagnostic';
}
if (error.link) {
return 'sign.txt.howToFix';
}
return 'sign.txt.tryAgain';
};
/**
* Получение компонента ссылки.
*/
const getCmp = (error: IError, hashLink: string) => {
if (isCm1Error(error)) {
return [<DiagnosticLink key="diagnostic" />];
}
if (error.link) {
return [<HowToLink key="how-to" hashLink={hashLink} />];
}
return [];
};
/**
* Генерация ошибки со ссылкой на базу знаний.
*/
export const generateCryptoError = (error: IError) => {
const hasCm1Error = isCm1Error(error);
const values = {
code: error.code,
desc: error.description ? `${error.description}. ` : '',
message: hasCm1Error ? '' : t('sign.txt.callUs', { code: CODE_MAP[error.code] || error.code }),
hashLink: getLinkHash(error),
};
const key = getKey(error);
const components = getCmp(error, values.hashLink);
return {
i18nKey: key,
components,
values,
parent: Text.P2,
};
};
/**
* Метод получения типа дистрибутива КМ.
*/
export const getCmType = (type?: string): string => {
if (type) return type;
if (isWin()) return 'win';
const config = settingsStore.systemSettings.get(SETTING_CODE.SETTINGS) as Record<string, any>;
if (!config) {
return '';
}
const { cmForMacEnabled } = config;
if (cmForMacEnabled && isMac()) return 'mac';
return 'nix';
};
+24
View File
@@ -0,0 +1,24 @@
import { detect } from 'detect-browser';
const getOs = () => detect()?.os?.toLowerCase() || '';
/**
* Проверка платформы пользователя.
*
* @returns True, если ОС семейства Linux, false в остальных случаях.
*/
export const isLinux = () => getOs().includes('linux');
/**
* Проверка платформы пользователя.
*
* @returns True, если ОС Windows, false в остальных случаях.
*/
export const isWin = () => getOs().includes('windows');
/**
* Проверка платформы пользователя.
*
* @returns True, если MacOS, false в остальных случаях.
*/
export const isMac = () => getOs().includes('mac');
+15
View File
@@ -0,0 +1,15 @@
import type { AxiosResponse } from 'axios';
import type { IServerDataResp } from '../interfaces';
/**
* Метод обработки ответа.
*
* @throws Ошибка.
*/
export const handleDataResponse = <T>(response: AxiosResponse<IServerDataResp<T>>) => {
if (response.data.error) {
throw response.data.error;
}
return response.data.data;
};
+27
View File
@@ -0,0 +1,27 @@
/**
* Область приложения.
*/
export enum SCOPE {
/**
* Пользователь Клиента.
*/
CLIENT = 'CLIENT',
/**
* Пользователь Банка.
*/
ADMIN = 'ADMIN',
}
let scope = SCOPE.CLIENT;
/**
* Метод установки скоупа библиотеки.
*/
export const setCryptoScope = (defaultScope: SCOPE) => {
scope = defaultScope;
};
/**
* Метод проверки клиентского скоупа.
*/
export const isClientScope = () => scope === SCOPE.CLIENT;
+37
View File
@@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import type { AxiosError } from 'axios';
import { captureException } from '@eco/monitoring';
import { PLUGIN_NETWORK_ERROR_MESSAGE, VERSION_ERROR_MESSAGE } from '../stream-constants/crypto';
const CRYPTO_MODULE_ERROR_TYPE_TAG = 'crypto-module-error-type';
const CRYPTO_MODULE_EXCEPTION_NAME = 'CryptoModuleError';
enum CRYPTO_MODULE_ERROR_TYPE {
NOT_FOUND = 'crypto-module-not-found',
VERSION_INVALID = 'crypto-module-version-invalid',
WITH_CODE = 'crypto-module-error-with-code',
UNKNOWN = 'crypto-module-unknown-error',
}
const ERROR_MESSAGE_TO_ERROR_TYPE = {
[PLUGIN_NETWORK_ERROR_MESSAGE]: CRYPTO_MODULE_ERROR_TYPE.NOT_FOUND,
[VERSION_ERROR_MESSAGE]: CRYPTO_MODULE_ERROR_TYPE.VERSION_INVALID,
};
const getErrorType = (error: AxiosError<any>) => {
if (error?.response?.data?.code) {
return CRYPTO_MODULE_ERROR_TYPE.WITH_CODE;
}
return ERROR_MESSAGE_TO_ERROR_TYPE[error.message] || CRYPTO_MODULE_ERROR_TYPE.UNKNOWN;
};
export const captureCryptoModuleException = (error: AxiosError<any>) => {
captureException(CRYPTO_MODULE_EXCEPTION_NAME, hint => {
hint.setTag(CRYPTO_MODULE_ERROR_TYPE_TAG, getErrorType(error));
hint.setExtra('Response', error);
return hint;
});
};
+40
View File
@@ -0,0 +1,40 @@
import type { IOlkInitOptions } from '../interfaces/olk';
import { cryptoModuleService } from '../services/crypto-module';
import { CONTAINER_TYPE, CRYPTO_MODULE_SIGN_TYPE } from '../stream-constants/olk';
import { getCertificate, setCertificate, setCertificateLocation } from './certificate';
let sessionId = '';
export const getSessionId = () => sessionId;
export const setSessionId = (newSessionId: string) => {
sessionId = newSessionId;
};
export const initSession = async ({ containerNameLocation, certData }: IOlkInitOptions) => {
if (sessionId && getCertificate() === certData) {
return sessionId;
}
/**
* Если providerInfo не передали, то используется для Windows КриптоПРО CSP, для MAC - КриптоПРО JCP.
*/
const containerInfo = containerNameLocation
? {
'@type': CONTAINER_TYPE.DBOBSS,
containerLocationName: containerNameLocation,
}
: undefined;
const { keySessID } = await cryptoModuleService.getInitkey({
containerInfo,
signType: CRYPTO_MODULE_SIGN_TYPE.CMS,
X509data: certData,
});
setCertificate(certData);
setSessionId(keySessID);
setCertificateLocation(containerNameLocation);
return keySessID;
};
+33
View File
@@ -0,0 +1,33 @@
import { localization } from '@msb/localization';
import type { IUserName } from '../interfaces';
import { UserType } from '../interfaces';
import { LOCALE_NAME } from '../stream-constants';
const t = localization.translate(LOCALE_NAME);
/**
* Получение локализации типа пользователя.
*
* @param type - Тип пользователя.
*/
export const getLocalizationUserType = (type?: UserType): string => {
const map = {
get [UserType.BANK]() {
return t('modal.historyChangesDialog.txt.bank');
},
get [UserType.CLIENT]() {
return t('modal.historyChangesDialog.txt.client');
},
get [UserType.TECHNICAL]() {
return t('modal.historyChangesDialog.txt.technical');
},
};
return type ? map[type] : '';
};
/**
* Получение строкового представления ФИО.
*/
export const getFio = ({ familyName, firstName, middleName }: IUserName) =>
[familyName, firstName, middleName].filter(chunk => !!chunk).join(' ');
+4
View File
@@ -0,0 +1,4 @@
export * from './core';
export * from './install';
export * from './sign';
export * from './verify';
@@ -0,0 +1,152 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTheme } from '@emotion/react';
import { BaseModal, ModalSheet, STATUS_ICON } from '@fractal-ui/overlays';
import { useBreakpoints } from '@fractal-ui/styling';
import type { FractalUiTheme, StatusIconType } from '@fractal-ui/styling';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { ModalFooter, downloadCryptoPlugin, LINUX_TYPES, LOCALE_NAME, isLinux, CheckCryptoView } from '../../core';
import type { ModalButtonProps, Installer } from '../../core';
import { STEP, SOURCES, HEADER, BUTTON_TEXT } from '../constants';
import type { CryptoInstallerProps } from '../interfaces';
import { getActions } from '../utils';
import { Info } from './info';
import { InstallResult } from './install-result';
const Views = {
[STEP.CHECK_INSTALL]: CheckCryptoView,
[STEP.INSTALL]: Info,
[STEP.RESULT]: InstallResult,
};
/**
* МО установки КМ.
*/
export const CryptoInstallerModal: React.FC<CryptoInstallerProps> = ({
linkText,
linkUrl,
onSuccess,
onClose,
isOpen,
source = SOURCES.OTHER,
}) => {
const router = useHistory();
const { t } = useTranslation(LOCALE_NAME);
const [step, setStep] = useState<STEP>(STEP.CHECK_INSTALL);
const { XS } = useBreakpoints();
const Component = XS ? ModalSheet : BaseModal;
const theme = useTheme() as unknown as FractalUiTheme;
const iconStyle = (theme?.colors?.modal?.statusIcon ?? 'light') as StatusIconType;
const IconWarning = STATUS_ICON[iconStyle].warning;
useEffect(() => {
if (isOpen) {
setStep(STEP.CHECK_INSTALL);
}
}, [isOpen]);
const next = useCallback(() => {
switch (step) {
case STEP.CHECK_INSTALL:
setStep(STEP.INSTALL);
return;
case STEP.INSTALL:
setStep(STEP.RESULT);
return;
default:
return;
}
}, [step]);
const header = HEADER[step];
const View = Views[step];
const secondaryButtonText = t(BUTTON_TEXT[source]);
const contextItems = LINUX_TYPES.map(type => ({
text: t(`sign.modal.cryptoInstallerModal.btn.${type}`),
onClick: () => {
void downloadCryptoPlugin(type as keyof Installer);
next();
},
}));
const cancelBtn: ModalButtonProps = {
dataAction: 'CLOSE_OR_REDIRECT',
onClick: () => {
linkUrl ? router?.push(linkUrl) : onClose?.();
},
text: linkText ?? secondaryButtonText,
actionBtnType: 'link',
};
const checkActions: ModalButtonProps[] = isLinux()
? [
{
dataAction: 'INSTALL',
text: t('sign.modal.cryptoInstallerModal.btn.installCryptomodul'),
preventClose: true,
actionBtnType: 'dropdown',
items: contextItems,
},
cancelBtn,
]
: [
{
dataAction: 'INSTALL',
onClick: () => {
void downloadCryptoPlugin();
next();
},
text: t('sign.modal.cryptoInstallerModal.btn.installCryptomodul'),
preventClose: true,
actionBtnType: 'button',
},
cancelBtn,
];
const installActions: ModalButtonProps[] = [cancelBtn];
const resultActions: ModalButtonProps[] = [
{
dataAction: 'TRY_AGAIN',
onClick: () => {
onSuccess?.();
onClose?.();
},
text: t('sign.modal.cryptoInstallerModal.btn.tryAgain'),
preventClose: true,
actionBtnType: 'button',
},
cancelBtn,
];
const actions = getActions({
checkActions,
resultActions,
installActions,
step,
});
return (
<Component
actionsDirection="vertical"
align="center"
footer={<ModalFooter actions={actions} actionsDirection="vertical" onClose={onClose} />}
header={t(header) ?? ''}
icon={IconWarning}
isOpen={isOpen}
padding="modal.status.M"
width="modal.status.width.M"
onClose={onClose}
>
<View next={next} source={source} onClose={onClose} />
</Component>
);
};
CryptoInstallerModal.displayName = 'CryptoInstallerModal';
@@ -0,0 +1,3 @@
export * from './info';
export * from './crypto-installer-modal';
export * from './install-result';
@@ -0,0 +1,61 @@
import React, { useEffect, useRef } from 'react';
import { ProgressDots } from '@fractal-ui/core';
import { Text } from '@fractal-ui/styling';
import { to } from '@msb/http';
import { useTranslation } from 'react-i18next';
import { olkCryptoModule as cryptoModule, LOCALE_NAME } from '../../core';
import { INTERVAL_CHECK_CRYPTO, AWAIT_TIME_CRYPTO_INSTALL, SOURCES } from '../constants';
import type { IViewProps } from '../interfaces';
import { LoaderWrapper } from './styled';
const CONTENT = {
[SOURCES.DST]: 'sign.modal.cryptoInstallerModal.txt.installCryptomodulForRegCert',
[SOURCES.VERIFY]: 'sign.modal.cryptoInstallerModal.txt.installCryptomodulForCheckSignature',
[SOURCES.OTHER]: 'sign.modal.cryptoInstallerModal.txt.installCryptomodulForSign',
};
/**
* Ожидание установки КМ.
*/
export const Info: React.FC<IViewProps> = ({ source, next, onClose }) => {
const { t } = useTranslation(LOCALE_NAME);
const timerIsRunning = useRef(false);
useEffect(() => {
if (timerIsRunning.current) return;
timerIsRunning.current = true;
const jobId = setInterval(async () => {
const [installed] = await to(cryptoModule.installationCheck());
if (installed) {
next();
}
}, INTERVAL_CHECK_CRYPTO);
const waitingTimeout = setTimeout(() => {
clearInterval(jobId);
onClose?.();
}, AWAIT_TIME_CRYPTO_INSTALL);
return () => {
timerIsRunning.current = false;
clearInterval(jobId);
clearTimeout(waitingTimeout);
};
}, [next, onClose]);
return (
<>
<Text.P2 color={'text.secondary'}>{t(CONTENT[source])}</Text.P2>
<br />
<LoaderWrapper>
<ProgressDots />
</LoaderWrapper>
</>
);
};
Info.displayName = 'Info';
@@ -0,0 +1,23 @@
import React from 'react';
import { Text } from '@fractal-ui/styling';
import { useTranslation } from 'react-i18next';
import { LOCALE_NAME } from '../../core';
import { SOURCES } from '../constants';
import type { IViewProps } from '../interfaces';
const CONTENT = {
[SOURCES.DST]: 'sign.modal.cryptoInstallerModal.txt.tryAgainToRegCert',
[SOURCES.VERIFY]: 'sign.modal.cryptoInstallerModal.txt.tryAgainToCheckSignature',
[SOURCES.OTHER]: 'sign.modal.cryptoInstallerModal.txt.tryAgainToSign',
};
/**
* Компонент отобажающий результат установки КМ.
*/
export const InstallResult: React.FC<IViewProps> = ({ source }) => {
const { t } = useTranslation(LOCALE_NAME);
return <Text.P2 color={'text.secondary'}>{t(CONTENT[source])}</Text.P2>;
};
InstallResult.displayName = 'InstallResult';
@@ -0,0 +1,11 @@
import styled from '@emotion/styled';
import styledCss from '@styled-system/css';
import type { FlexboxProps, LayoutProps } from 'styled-system';
/** Контейнер для компонента прелоадер. */
export const LoaderWrapper = styled.div<FlexboxProps & LayoutProps>(
styledCss({
display: 'flex',
justifyContent: 'center',
})
);
@@ -0,0 +1,44 @@
export enum STEP {
CHECK_INSTALL = 'CHECK_INSTALL',
INSTALL = 'INSTALL',
RESULT = 'RESULT',
}
/**
* Заголовок МО для каждого шага.
*/
export const HEADER = {
[STEP.CHECK_INSTALL]: 'sign.modal.cryptoInstallerModal.txt.title',
[STEP.INSTALL]: 'sign.modal.cryptoInstallerModal.txt.waitingForCMReply',
[STEP.RESULT]: 'sign.modal.cryptoInstallerModal.txt.CryptomodulIsInstalled',
};
export const INTERVAL_CHECK_CRYPTO = 3000;
export const AWAIT_TIME_CRYPTO_INSTALL = 5 * 60 * 1000;
/**
* Источник вызова МО.
*/
export enum SOURCES {
/**
* СЭП.
*/
DST = 'dst',
/**
* Проверка подписи.
*/
VERIFY = 'verify',
/**
* Другое.
*/
OTHER = 'other',
}
/**
* Текст кнопки в зависимости от источника вызова МО.
*/
export const BUTTON_TEXT = {
[SOURCES.DST]: 'sign.modal.cryptoInstallerModal.btn.toSignatureList',
[SOURCES.VERIFY]: 'btn.close',
[SOURCES.OTHER]: 'sign.btn.toList',
};
@@ -0,0 +1 @@
export * from './common';
+1
View File
@@ -0,0 +1 @@
export * from './use-crypto-installer';
@@ -0,0 +1,42 @@
import { useState, useCallback } from 'react';
import { CryptoInstallerModal } from '../components/crypto-installer-modal';
import type { CryptoInstallerProps } from '../interfaces';
/**
* Хук для отображения МО установки КМ.
*/
export const useCryptoInstaller = () => {
const [installerProps, setInstallerProps] = useState<CryptoInstallerProps>({});
const handleClose = () => {
setInstallerProps(props => ({
...props,
isOpen: false,
}));
};
const showCryptoInstaller = useCallback(
(props?: CryptoInstallerProps) =>
new Promise((resolve, reject) => {
setInstallerProps({
...props,
isOpen: true,
onSuccess: () => {
resolve(true);
handleClose();
},
onClose: () => {
reject();
handleClose();
},
});
}),
[]
);
return {
showCryptoInstaller,
CryptoInstallerModal,
props: installerProps,
};
};
+4
View File
@@ -0,0 +1,4 @@
export * from './constants';
export * from './components';
export * from './hooks';
export type { CryptoInstallerProps, SourceType } from './interfaces';
@@ -0,0 +1,43 @@
import type { ModalProps } from '@fractal-ui/overlays';
import type { ModalButtonProps } from '../../core';
import type { STEP } from '../constants';
export type SourceType = 'dst' | 'other' | 'verify';
/** Свойства диалога установки КМ. */
export interface CryptoInstallerProps extends Pick<ModalProps, 'isOpen' | 'onClose'> {
/** Текст дополнительной кнопки. */
linkText?: string;
/** Адрес для перехода по клику на дополнительной кнопке. */
linkUrl?: string;
/**
* Источник вызова МО.
*/
source?: SourceType;
/**
* Колбэк успешного завершения.
*/
onSuccess?(): void;
}
/**
* Свойства формы.
*/
export interface IViewProps extends Pick<ModalProps, 'onClose'>, Required<Pick<CryptoInstallerProps, 'source'>> {
/**
* Метод перехода на следующий шаг.
*/
next(): void;
}
/** Свойства функции получения действий по шагу формы. */
export interface GetActionProps {
/** Действие шага проверки. */
checkActions: ModalButtonProps[];
/** Действия шага установки КМ. */
installActions: ModalButtonProps[];
/** Действия шага результата установки КМ. */
resultActions: ModalButtonProps[];
/** Шаг формы. */
step: STEP;
}
@@ -0,0 +1 @@
export * from './common';
+17
View File
@@ -0,0 +1,17 @@
import { STEP } from '../constants';
import type { GetActionProps } from '../interfaces';
/**
* Метод получение экшенов МО установки КМ в зависимости от шага.
*/
export const getActions = ({ checkActions, installActions, resultActions, step }: GetActionProps) => {
switch (step) {
case STEP.INSTALL:
return installActions;
case STEP.RESULT:
return resultActions;
default:
return checkActions;
}
};
+1
View File
@@ -0,0 +1 @@
export * from './common';
+37
View File
@@ -0,0 +1,37 @@
{
"name": "@msb/crypto",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"dependencies": {
"react": "17.0.2",
"react-dom": "17.0.2",
"react-router-dom": "5.2.0",
"detect-browser": "5.2.1",
"react-i18next": "11.2.7",
"i18next": "22.5.1",
"@msb/http": "1.0.0",
"@msb/shared": "1.0.0",
"@msb/localization": "1.0.0",
"axios": "1.9.0",
"yup": "0.32.9",
"bignumber.js": "9.0.1",
"@eco/monitoring": "2.2.2",
"semver": "7.5.2",
"styled-system": "5.1.5",
"@emotion/styled": "11.8.1",
"@fractal-ui/composites": "30.2.0",
"@fractal-ui/core": "30.2.0",
"@fractal-ui/library": "30.2.0",
"@fractal-ui/overlays": "30.2.0",
"@fractal-ui/styling": "30.1.0"
},
"devDependencies": {
"@types/semver": "7.3.9"
},
"peerDependencies": {
"react": "17.0.2",
"react-dom": "17.0.2",
"react-router-dom": "5.2.0"
}
}
@@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { Informer } from '@fractal-ui/extended';
import { Modal, type ModalButtonProps } from '@fractal-ui/overlays';
import { Wrapper as Space } from '@fractal-ui/styling';
import { addCmpToPortal, removeCmpFromPortal } from '@msb/shared';
import { useTranslation } from 'react-i18next';
import { LOCALE_NAME } from '../../../core';
import type { ICancelModal } from './types';
const CancelModal: React.FC<ICancelModal> = () => {
const [isOpened, setIsOpened] = useState(true);
const { t } = useTranslation(LOCALE_NAME);
const handleClose = () => {
setIsOpened(false);
removeCmpFromPortal('CancelModal');
};
const handleAccept = () => {
setIsOpened(false);
};
const actions: ModalButtonProps[] = [
{
dataAction: 'close',
onClick: handleClose,
size: 'L',
variant: 'blue',
width: 'auto',
text: t('btn.cancel'),
},
{
dataAction: 'continue',
onClick: handleAccept,
size: 'L',
variant: 'primary',
width: 'auto',
text: t('sign.modal.fractalSignModal.cancelProcessing.btn.batchProcessing'),
},
];
return (
<Modal actions={actions} header={t('sign.modal.fractalSignModal.cancelProcessing.txt.cancel') ?? ''} isOpen={isOpened} width="600px">
<Space mt={'5'} />
<Informer statusIcon variant="warning">
{t('sign.modal.fractalSignModal.cancelProcessing.txt.desc') ?? ''}
</Informer>
</Modal>
);
};
export const openCancelModal = () => addCmpToPortal('CancelModal', CancelModal);
CancelModal.displayName = 'CancelModal';
@@ -0,0 +1,14 @@
/**
* Свойства компонента закрытия МО.
*/
export interface ICancelModal {
/**
* Метод закрытия окна.
*/
onClose(): void;
/**
* Метод продолжения установки подписи.
*/
onAccept(): void;
}
+1
View File
@@ -0,0 +1 @@
export * from './cancel';
@@ -0,0 +1,26 @@
import React from 'react';
import { useBreakpoints, Wrapper, Text } from '@fractal-ui/styling';
import { ModalFooter } from '../../core';
import type { ActionsFooterProps } from './types';
/**
* Футер с кнопками модалки подписи.
*
* Обертка над ModalFooter с добавлением текста на XS.
*/
export const SignModalFooter: React.FC<ActionsFooterProps> = ({ description, ...props }) => {
const { XS } = useBreakpoints();
return (
<>
<ModalFooter {...props} />
{XS && description && (
<>
<Wrapper mt="4" />
<Text.P3 color="text.secondary" textAlign="left">
{description}
</Text.P3>
</>
)}
</>
);
};
@@ -0,0 +1,6 @@
export * from './pictograms';
export * from './sign-details';
export * from './sign-tool';
export * from './sign-tools-wrapper';
export * from './sign-header';
export * from './statistic-amount';
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More