├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── Dockerfile.test ├── README.md ├── cryptopro ├── certificates │ └── certificate_bundle_example.zip ├── esia │ ├── esia_prod.cer │ └── esia_test.cer ├── install │ └── .gitkeep └── scripts │ ├── lib │ ├── colors.sh │ └── functions.sh │ ├── setup_license │ ├── setup_my_certificate │ ├── setup_root │ └── sign ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── app.module.ts ├── config.ts ├── controllers │ ├── controllers.module.ts │ ├── cryptopro.controller.ts │ ├── dto │ │ ├── ok.response.dto.ts │ │ ├── probe.dto.ts │ │ ├── sign.dto.ts │ │ └── status.dto.ts │ └── status.controller.ts ├── main.ts ├── middleware │ ├── http.exception.filter.ts │ ├── logger.interceptor.ts │ └── result.interceptor.ts ├── types │ └── errors.ts └── utils │ ├── cryptoProUtils.ts │ └── logUtils.ts ├── test ├── cryptopro.controller.spec.ts └── status.controller.spec.ts ├── tsconfig.build.json ├── tsconfig.json └── versions.json /.eslintignore: -------------------------------------------------------------------------------- 1 | docker 2 | dist 3 | .eslintrc.js 4 | cryptopro -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: './tsconfig.json', 4 | requireConfigFile: false, 5 | }, 6 | extends: [ 7 | '@wavesenterprise/eslint-config/typescript-mixed', 8 | ], 9 | rules: { 10 | "no-empty-function": "off", 11 | "no-redeclare": "off" 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | dist/ 4 | distTs/ 5 | cryptopro/certificates/certificate_bundle.zip 6 | .env -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Базовый образ с КриптоПро 2 | #FROM debian:stretch-slim as cryptopro-generic 3 | FROM node:stretch-slim as cryptopro-generic 4 | 5 | # Устанавливаем timezone 6 | ENV TZ="Europe/Moscow" \ 7 | docker="1" 8 | 9 | ARG LICENSE 10 | ENV LICENSE ${LICENSE} 11 | 12 | # prod или test 13 | ARG ESIA_ENVIRONMENT='test' 14 | ENV ESIA_CORE_CERT_FILE "/cryptopro/esia/esia_${ESIA_ENVIRONMENT}.cer" 15 | ENV ESIA_PUB_KEY_FILE "/cryptopro/esia/esia_${ESIA_ENVIRONMENT}.pub" 16 | 17 | ARG CERTIFICATE_PIN 18 | ENV CERTIFICATE_PIN ${CERTIFICATE_PIN} 19 | 20 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ 21 | echo $TZ > /etc/timezone 22 | 23 | ADD cryptopro/install /tmp/src 24 | RUN cd /tmp/src && \ 25 | tar -xf linux-amd64_deb.tgz && \ 26 | linux-amd64_deb/install.sh && \ 27 | # делаем симлинки 28 | cd /bin && \ 29 | ln -s /opt/cprocsp/bin/amd64/certmgr && \ 30 | ln -s /opt/cprocsp/bin/amd64/cpverify && \ 31 | ln -s /opt/cprocsp/bin/amd64/cryptcp && \ 32 | ln -s /opt/cprocsp/bin/amd64/csptest && \ 33 | ln -s /opt/cprocsp/bin/amd64/csptestf && \ 34 | ln -s /opt/cprocsp/bin/amd64/der2xer && \ 35 | ln -s /opt/cprocsp/bin/amd64/inittst && \ 36 | ln -s /opt/cprocsp/bin/amd64/wipefile && \ 37 | ln -s /opt/cprocsp/sbin/amd64/cpconfig && \ 38 | # прибираемся 39 | rm -rf /tmp/src 40 | 41 | RUN apt-get update && apt-get install -y --no-install-recommends expect libboost-dev unzip g++ curl 42 | 43 | ADD cryptopro/scripts /cryptopro/scripts 44 | ADD cryptopro/certificates /cryptopro/certificates 45 | ADD cryptopro/esia cryptopro/esia 46 | 47 | FROM cryptopro-generic as configured-cryptopro 48 | 49 | # устанавливаем лицензию, если она указана 50 | RUN ./cryptopro/scripts/setup_license ${LICENSE} 51 | 52 | # Устанавливаем корневой сертификат есиа 53 | RUN ./cryptopro/scripts/setup_root ${ESIA_CORE_CERT_FILE} 54 | 55 | # Устанавливаем сертификат пользователя 56 | RUN ./cryptopro/scripts/setup_my_certificate /cryptopro/certificates/certificate_bundle.zip ${CERTIFICATE_PIN} 57 | 58 | FROM configured-cryptopro 59 | COPY package.json . 60 | COPY package-lock.json . 61 | COPY tsconfig.json . 62 | COPY tsconfig.build.json . 63 | COPY versions.json . 64 | COPY nest-cli.json . 65 | COPY src . 66 | RUN npm ci -q 67 | RUN npm run build 68 | RUN npm prune --production 69 | 70 | EXPOSE 3037 71 | #CMD ["sleep", "100000000000"] 72 | CMD npm start 73 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | # Базовый образ с КриптоПро 2 | #FROM debian:stretch-slim as cryptopro-generic 3 | FROM node:stretch-slim as cryptopro-generic 4 | 5 | # Устанавливаем timezone 6 | ENV TZ="Europe/Moscow" \ 7 | docker="1" 8 | 9 | ARG LICENSE 10 | ENV LICENSE ${LICENSE} 11 | 12 | # prod или test 13 | ARG ESIA_ENVIRONMENT='test' 14 | ENV ESIA_CORE_CERT_FILE "/cryptopro/esia/esia_${ESIA_ENVIRONMENT}.cer" 15 | 16 | ARG CERTIFICATE_PIN 17 | ENV CERTIFICATE_PIN ${CERTIFICATE_PIN} 18 | 19 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ 20 | echo $TZ > /etc/timezone 21 | 22 | ADD cryptopro/install /tmp/src 23 | RUN cd /tmp/src && \ 24 | tar -xf linux-amd64_deb.tgz && \ 25 | linux-amd64_deb/install.sh && \ 26 | # делаем симлинки 27 | cd /bin && \ 28 | ln -s /opt/cprocsp/bin/amd64/certmgr && \ 29 | ln -s /opt/cprocsp/bin/amd64/cpverify && \ 30 | ln -s /opt/cprocsp/bin/amd64/cryptcp && \ 31 | ln -s /opt/cprocsp/bin/amd64/csptest && \ 32 | ln -s /opt/cprocsp/bin/amd64/csptestf && \ 33 | ln -s /opt/cprocsp/bin/amd64/der2xer && \ 34 | ln -s /opt/cprocsp/bin/amd64/inittst && \ 35 | ln -s /opt/cprocsp/bin/amd64/wipefile && \ 36 | ln -s /opt/cprocsp/sbin/amd64/cpconfig && \ 37 | # прибираемся 38 | rm -rf /tmp/src 39 | 40 | RUN apt-get update && apt-get install -y --no-install-recommends expect libboost-dev unzip g++ curl 41 | 42 | ADD cryptopro/scripts /cryptopro/scripts 43 | ADD cryptopro/certificates /cryptopro/certificates 44 | ADD cryptopro/esia cryptopro/esia 45 | 46 | FROM cryptopro-generic as configured-cryptopro 47 | 48 | # устанавливаем лицензию, если она указана 49 | RUN ./cryptopro/scripts/setup_license ${LICENSE} 50 | 51 | # Устанавливаем корневой сертификат есиа 52 | RUN ./cryptopro/scripts/setup_root ${ESIA_CORE_CERT_FILE} 53 | 54 | # Устанавливаем сертификат пользователя 55 | RUN ./cryptopro/scripts/setup_my_certificate /cryptopro/certificates/certificate_bundle.zip ${CERTIFICATE_PIN} 56 | 57 | FROM configured-cryptopro 58 | COPY package.json . 59 | COPY package-lock.json . 60 | COPY tsconfig.json . 61 | COPY tsconfig.build.json . 62 | COPY .eslintrc.js .eslintrc.js 63 | COPY .eslintignore .eslintignore 64 | COPY versions.json versions.json 65 | COPY src src 66 | COPY test test 67 | 68 | RUN npm ci -q 69 | 70 | CMD npm run test-ci 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Сервис для создания подписей для ЕСИА на nodejs с КриптоПро 4.0 в докер контейнере 2 | 3 | ## Принцип работы 4 | * Устанавливает КриптоПро из установщика `cryptopro/install/linux-amd64_deb.tgz` 5 | * Устанавливает лицензию КриптоПро 6 | * Загружает корневой сертификат есиа 7 | * Загружает пользовательский сертификат 8 | * Запускает рест сервер со swagger и методом создания подписей 9 | 10 | ### Установка лицензии 11 | Лицензия устанавливается из аргумента `LICENSE`, если не указана используется триал версия(работает 3 месяца). 12 | 13 | ### Загрузка корневого сертификата 14 | в зависимости от аргумента `ESIA_ENVIRONMENT` загружается сертификат нужного окружения есиа: 15 | * Если не указан или указан `test` - сертификат тестового контура есиа (https://esia-portal1.test.gosuslugi.ru) 16 | * Если указан `prod` - сертификат от основного есиа (https://esia.gosuslugi.ru) 17 | 18 | Корневые сертификаты есиа лежат в папке `cryptopro/esia` 19 | 20 | ### Загрузка пользовательского сертификата 21 | Необходимо специальным образом сформировать zip-архив `certificate_bundle.zip` и положить его в папку `/cryptopro/certificates`. 22 | Пример такого zip-файла лежит в той же директории под названием `certificate_bundle_example.zip` 23 | Содержимое zip файла: 24 | ``` 25 | ├── certificate.cer - файл сертификата 26 | └── le-09650.000 - каталог с файлами закрытого ключа 27 | ├── header.key 28 | ├── masks2.key 29 | ├── masks.key 30 | ├── name.key 31 | ├── primary2.key 32 | └── primary.key 33 | ``` 34 | Первый найденный файл в корне архива будет воспринят как сертификат, а первый найденный каталог - как связка файлов закрытого ключа. Пароль от контейнера, если есть, передается аргументом `CERTIFICATE_PIN` 35 | 36 | ### Как запустить 37 | 1. Скачать [КриптоПро CSP 4.0 для Linux (x64, deb)](https://www.cryptopro.ru/products/csp/downloads) и положить по пути `install/linux-amd64_deb.tgz` 38 | 2. Подложить архив с сертификатом как `/cryptopro/certificates/certificate_bundle.zip` 39 | 3. Создаем образ `docker build --tag cryptopro-sign --build-arg CERTIFICATE_PIN=12345678 .` 40 | 4. Запускаем `docker run -it --rm -p 3037:3037 --name cryptopro-sign cryptopro-sign` 41 | 42 | ### Рест сервер 43 | Если следовать инструкции выше, то свагер будет находиться по адресу `http://localhost:3037/docs/#/` 44 | Там же есть примеры вызова методов. 45 | Доступные методы: 46 | * `POST /cryptopro/sign` - подписать текст 47 | 48 | ### Как по быстрому выпустить тестовый сертификат: 49 | * Запустить докер контейнер по инструкции выше 50 | * Заходим в запущенный контейнер `docker exec -ti cryptopro-sign sh` 51 | * Создаем запрос на сертификат `cryptcp -creatrqst -dn 'cn=test' -cont '\\.\hdimage\test2' -pin 12345678 tmp/test.csr` (попросит понажимать разные клавиши) 52 | * Выводим результат `cat /tmp/test.csr` 53 | * Заходим на `http://www.cryptopro.ru/certsrv/certrqxt.asp` и вставляем вывод 54 | * В следующем окне выбираем `Base64-шифрование` и `Скачать сертификат` 55 | * Качаем и сохраняем `certnew.cer` файл в проекте по пути `cryptopro/certificates/certnew.cer` 56 | * В отдельном терминале переносим файл в запущенный контейнер `docker cp cryptopro/certificates/certnew.cer cryptopro-sign:tmp/test.cer` 57 | * Возвращаемся в первый терминал и загружаем сертификат в КриптоПро `cryptcp -instcert -cont '\\.\hdimage\test2' tmp/test.cer` 58 | * Попросит ввести пароль. Вводим `12345678` 59 | * Переносим на нашу машину приватные ключи `docker cp cryptopro-sign:var/opt/cprocsp/keys/root/test2.000 cryptopro/certificates/test2.000` 60 | * В папке проекта `cryptopro/certificates` создаем архив. В архив кладем папку `test2.000` и файл `certnew.cer` 61 | * Архив называем `certificate_bundle.zip`, пересобираем докер образ и запускаем. 62 | 63 | Черпал вдохновение и взял многие вещи из [этого репозитория](https://github.com/dbfun/cryptopro) 64 | 65 | ### Авторизация 66 | Для использования в продакшене лучше, чтобы сервис был не доступен снаружи инфраструктуры, т.к тут через рест можно что угодно подписать зашитым сертификатом. Если все же инфраструктура открытая, то в сервис следует встроить проверку авторизации. 67 | 68 | ### Возможные проблемы: 69 | Если получаете код ошибки `0x80090010` при вызове метода sign - вероятно срок действия вашего сертификата истек. Попробуйте создать новый по инструкции. 70 | 71 | ### Env контейнера 72 | ``` 73 | PORT - numer. Порт рест сервера. Дефолт: 3037 74 | ``` -------------------------------------------------------------------------------- /cryptopro/certificates/certificate_bundle_example.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waves-enterprise/cryptopro-sign/967a64579259dfa27959ecf3ffeb1035413c73d4/cryptopro/certificates/certificate_bundle_example.zip -------------------------------------------------------------------------------- /cryptopro/esia/esia_prod.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waves-enterprise/cryptopro-sign/967a64579259dfa27959ecf3ffeb1035413c73d4/cryptopro/esia/esia_prod.cer -------------------------------------------------------------------------------- /cryptopro/esia/esia_test.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waves-enterprise/cryptopro-sign/967a64579259dfa27959ecf3ffeb1035413c73d4/cryptopro/esia/esia_test.cer -------------------------------------------------------------------------------- /cryptopro/install/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waves-enterprise/cryptopro-sign/967a64579259dfa27959ecf3ffeb1035413c73d4/cryptopro/install/.gitkeep -------------------------------------------------------------------------------- /cryptopro/scripts/lib/colors.sh: -------------------------------------------------------------------------------- 1 | # Библиотека цветов 2 | # Сброс 3 | Color_Off='\e[0m' # Text Reset 4 | Red='\e[1;31m' # Red 5 | Green='\e[1;32m' # Green 6 | Yellow='\e[1;33m' # Yellow 7 | Blue='\e[1;34m' # Blue 8 | Purple='\e[1;35m' # Purple 9 | Cyan='\e[1;36m' # Cyan 10 | White='\e[1;37m' # White 11 | UCyan='\e[4;36m' # Cyan 12 | -------------------------------------------------------------------------------- /cryptopro/scripts/lib/functions.sh: -------------------------------------------------------------------------------- 1 | function assert 2 | { 3 | SUBSTR=`echo "$1" | grep -q "$2"` 4 | if [ $? != 0 ]; then 5 | error "$3; Expected: $2" 6 | exit 1 7 | fi 8 | } 9 | 10 | function info 11 | { 12 | echo -e $Blue"$1"$Color_Off 13 | } 14 | 15 | function ok 16 | { 17 | echo -e $Green"$1"$Color_Off 18 | } 19 | 20 | function warning 21 | { 22 | echo -e $Yellow"$1"$Color_Off 23 | } 24 | 25 | function error 26 | { 27 | echo -e $Red"$1"$Color_Off 28 | } 29 | -------------------------------------------------------------------------------- /cryptopro/scripts/setup_license: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Загружаем лицензию 5 | # 6 | 7 | if [[ $1 ]]; 8 | then 9 | echo "Setting license from env" 10 | cpconfig -license -set "$1" 11 | else 12 | echo "No license found. Using trial" 13 | fi 14 | cpconfig -license -view 15 | -------------------------------------------------------------------------------- /cryptopro/scripts/setup_my_certificate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Установка сертификатов пользователя для проверки и подписания 5 | # 6 | 7 | cd "$(dirname "$0")" 8 | source lib/colors.sh 9 | source lib/functions.sh 10 | 11 | dir=`mktemp -d` 12 | cd "$dir" 13 | #cat - > "bundle.zip" 14 | echo "bundle file $1" 15 | unzip -q "$1" 16 | #rm bundle.zip 17 | 18 | # info "Temp dir: $dir" 19 | 20 | # проверка всех необходимых файлов в закрытом ключе 21 | function testprivk { 22 | [ ! -f "$contShortName/header.key" ] && error "File header.key not found in $contShortName" && exit 1 23 | [ ! -f "$contShortName/masks.key" ] && error "File masks.key not found in $contShortName" && exit 1 24 | [ ! -f "$contShortName/masks2.key" ] && error "File masks2.key not found in $contShortName" && exit 1 25 | [ ! -f "$contShortName/name.key" ] && error "File name.key not found in $contShortName" && exit 1 26 | [ ! -f "$contShortName/primary.key" ] && error "File primary.key not found in $contShortName" && exit 1 27 | [ ! -f "$contShortName/primary2.key" ] && error "File primary2.key not found in $contShortName" && exit 1 28 | } 29 | 30 | # для связки закрытого ключа с сертификатом необходимо указать полное наименование контейнера, которое находится в name.key 31 | # таким способом также можно выяснить название, но с cp1251 работает не устойчиво 32 | # function findContFullName { 33 | # contFullName=`csptest -keyset -enum_cont -verifycontext -unique | grep --fixed-strings "$contShortName" | cut -d '|' -f1` 34 | # if [ ! -n "$contFullName" ]; then 35 | # error "Proper container not found by short name: $contShortName" 36 | # exit 1 37 | # fi 38 | # } 39 | 40 | # xargs удаляет `./` из названия файла 41 | certFileName=`find -maxdepth 1 -type f \! -name . | head -n1 | xargs -I{} basename {}` 42 | contShortName=`find -maxdepth 1 -type d \! -name . | head -n1 | xargs -I{} basename {}` 43 | contFullName=`tail -c+5 "$contShortName/name.key"` 44 | 45 | # Есть контейнер, устанавливаем 46 | if [ -n "$contShortName" ]; then 47 | info "Key container short name: $contShortName" 48 | testprivk 49 | cp -R "$contShortName" /var/opt/cprocsp/keys/root/ 50 | if [ "$?" -eq "0" ]; then 51 | ok "Key container installed" 52 | fi 53 | fi 54 | 55 | # Есть сертификат 56 | if [ -n "$certFileName" ]; then 57 | 58 | if [ ! -n "$contShortName" ]; then 59 | # устанавливается только сертификат 60 | certmgr -inst -file "$certFileName" 61 | 62 | if [ "$?" -eq "1" ]; then 63 | ok "Certificate installed" 64 | warning "No PrivateKey Link" 65 | fi 66 | 67 | else 68 | 69 | # устанавливается сертификат + контейнер, нужно их связать ('PrivateKey Link') 70 | 71 | info "Key container full name: $contFullName" | iconv -fcp1251 -tutf-8 72 | 73 | if [ -z "$2" ]; then 74 | info "no PIN found" 75 | # нет PIN 76 | certmgr -inst -file "$certFileName" -cont '\\.\HDIMAGE\'"$contFullName" 77 | else 78 | info "use PIN to setup certificate" 79 | # есть PIN 80 | certmgr -inst -file "$certFileName" -pin "$2" -cont '\\.\HDIMAGE\'"$contFullName" 81 | fi 82 | 83 | fi 84 | 85 | 86 | fi 87 | 88 | rm -rf "$dir" 89 | -------------------------------------------------------------------------------- /cryptopro/scripts/setup_root: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect 2 | 3 | set file [lindex $argv 0] 4 | 5 | spawn certmgr -inst -all -store uroot -file $file 6 | 7 | # Иногда "заедает" на '(o)OK, (c)Cancel', пауза должна решить проблему 8 | sleep 1 9 | 10 | while { 1 } { 11 | 12 | expect { 13 | "(o)OK, (c)Cancel" { 14 | send -- "o\r" 15 | sleep 1 16 | } 17 | eof { exit 0 } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /cryptopro/scripts/sign: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Подписание документа 5 | # 6 | 7 | # containerHash должен быть заменен на актуальный во время создания образа из Dockerfile 8 | containerHash="" 9 | 10 | if [[ -z "$containerHash" ]]; then 11 | echo "containerHash is not specified" 12 | exit 1 13 | fi 14 | 15 | tmp=`mktemp` 16 | cat - > "$tmp" 17 | err=$(cryptcp -sign -thumbprint "$containerHash" -nochain -hashAlg "1.2.643.7.1.1.2.2" -detached -pin "$1" "$tmp" "$tmp.sig") 18 | signResult=$? 19 | if [ "$signResult" != "0" ]; then 20 | rm -f "$tmp" "$tmp.sig" 21 | echo "Error: $err" 22 | exit $signResult 23 | fi 24 | 25 | cat "$tmp.sig" 26 | rm -f "$tmp" "$tmp.sig" 27 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cryptopro-sign", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "WE Voting team", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "build:check": "npx tsc --noEmit", 12 | "start": "node dist/main.js", 13 | "dev": "nest start --watch", 14 | "lint": "npx eslint src/** test/** --ext .ts,.tsx", 15 | "test": "jest --verbose", 16 | "test-ci": "npm run lint && npm run build:check && npm run test" 17 | }, 18 | "dependencies": { 19 | "@nestjs/common": "^7.6.17", 20 | "@nestjs/core": "^7.6.17", 21 | "@nestjs/platform-express": "^7.6.17", 22 | "@nestjs/swagger": "^4.8.0", 23 | "axios": "^0.21.1", 24 | "class-transformer": "^0.4.0", 25 | "class-validator": "^0.13.1", 26 | "cross-fetch": "^3.0.6", 27 | "dotenv": "^9.0.2", 28 | "moment": "^2.29.1", 29 | "nestjs-redoc": "^2.2.1", 30 | "reflect-metadata": "^0.1.13", 31 | "rimraf": "^3.0.2", 32 | "rxjs": "^6.5.4", 33 | "swagger-ui-express": "^4.1.6", 34 | "tempy": "^1.0.1" 35 | }, 36 | "devDependencies": { 37 | "@nestjs/cli": "^7.6.0", 38 | "@nestjs/schematics": "^7.3.1", 39 | "@nestjs/testing": "^7.6.17", 40 | "@types/express": "^4.17.3", 41 | "@types/jest": "^26.0.23", 42 | "@types/node": "^15.6.0", 43 | "@types/supertest": "^2.0.11", 44 | "@typescript-eslint/eslint-plugin": "^4.24.0", 45 | "@typescript-eslint/parser": "^4.24.0", 46 | "@wavesenterprise/eslint-config": "^0.1.4", 47 | "eslint": "^7.26.0", 48 | "eslint-plugin-import": "^2.23.2", 49 | "jest": "^26.6.3", 50 | "supertest": "^6.1.3", 51 | "ts-jest": "^26.5.6", 52 | "ts-loader": "^9.2.1", 53 | "ts-node": "^9.1.1", 54 | "tsconfig-paths": "^3.9.0", 55 | "typescript": "^4.2.4" 56 | }, 57 | "jest": { 58 | "testTimeout": 1500, 59 | "moduleFileExtensions": [ 60 | "js", 61 | "json", 62 | "ts", 63 | "tsx" 64 | ], 65 | "rootDir": "test", 66 | "testRegex": ".spec.ts$", 67 | "transform": { 68 | "^.+\\.(ts|js)x?$": "ts-jest" 69 | }, 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node", 72 | "coverageThreshold": { 73 | "global": { 74 | "branches": 50, 75 | "functions": 50, 76 | "lines": 50 77 | } 78 | }, 79 | "coverageReporters": [ 80 | "text", 81 | "html" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ControllersModule } from './controllers/controllers.module' 3 | 4 | @Module({ 5 | imports: [ 6 | ControllersModule, 7 | ], 8 | controllers: [], 9 | providers: [], 10 | }) 11 | export class AppModule { 12 | } 13 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv' 2 | import { version } from '../package.json' 3 | import { readFileSync } from 'fs' 4 | import * as path from 'path' 5 | 6 | config() 7 | 8 | type BuildInfo = { 9 | BUILD_ID: string, 10 | GIT_COMMIT: string, 11 | DOCKER_TAG: string, 12 | VERSION: string, 13 | } 14 | 15 | function prepareBuildInfo(): BuildInfo { 16 | const buildDefault = { 17 | BUILD_ID: 'development', 18 | GIT_COMMIT: 'development', 19 | DOCKER_TAG: 'development', 20 | VERSION: version, 21 | } 22 | try { 23 | const info = readFileSync('versions.json').toString() 24 | return { 25 | ...buildDefault, 26 | ...JSON.parse(info), 27 | } 28 | 29 | } catch (err) { 30 | // eslint-disable-next-line no-console 31 | console.error('not found versions.json', err.message) 32 | } 33 | return buildDefault 34 | } 35 | 36 | function getEnv(name: string): V | string | undefined 37 | function getEnv(name: string, defaultValue: V): V | string 38 | function getEnv(name: string, defaultValue?: V): V | string | undefined { 39 | return process.env[name] ?? defaultValue 40 | } 41 | 42 | function parseLogLevel() { 43 | const logLevel = getEnv('LOG_LEVEL', 'debug,log,warn,error') 44 | return new Set(logLevel.trim() 45 | .replace(/ +/g, '') 46 | .split(',') 47 | .map((s) => s.trim())) 48 | } 49 | 50 | export const BUILD_INFO = prepareBuildInfo() 51 | export const ROOT_DIR = path.resolve(__dirname, '..') 52 | 53 | export const CERTIFICATE_PIN = getEnv('CERTIFICATE_PIN') 54 | 55 | // REST 56 | export const PORT = Number(getEnv('PORT', 3037)) 57 | export const SWAGGER_BASE_PATH = getEnv('SWAGGER_BASE_PATH', '/') 58 | 59 | export const LOG_LEVEL = parseLogLevel() 60 | 61 | export const isValidEnv = () => { 62 | const config = getActualEnv() 63 | 64 | return Object.keys(config).reduce((isValid: boolean, key: string) => { 65 | if (config[key] === undefined || Number.isNaN(config[key])) { 66 | // eslint-disable-next-line no-console 67 | console.error(`Please set config/env variable: ${key}`) 68 | return false 69 | } 70 | return isValid 71 | }, true) 72 | } 73 | 74 | export function getActualEnv(): { [key: string]: unknown } { 75 | return { 76 | CERTIFICATE_PIN, 77 | LOG_LEVEL, 78 | PORT, 79 | SWAGGER_BASE_PATH, 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/controllers/controllers.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { StatusController } from './status.controller' 3 | import { CryptoProController } from './cryptopro.controller' 4 | 5 | @Module({ 6 | imports: [], 7 | controllers: [ 8 | StatusController, 9 | CryptoProController, 10 | ], 11 | }) 12 | export class ControllersModule { 13 | } 14 | -------------------------------------------------------------------------------- /src/controllers/cryptopro.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common' 2 | import { ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger' 3 | import { SignReqDto, SignResDto } from './dto/sign.dto' 4 | import { cryptoProSign } from '../utils/cryptoProUtils' 5 | 6 | @Controller() 7 | @ApiBearerAuth() 8 | export class CryptoProController { 9 | 10 | @Post('cryptopro/sign') 11 | @ApiOperation({ summary: 'Sign plain text with cryptopro' }) 12 | @ApiResponse({ type: SignResDto }) 13 | sign(@Body() { text }: SignReqDto) { 14 | return cryptoProSign(text) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/controllers/dto/ok.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | 3 | export class OKResp { 4 | @ApiProperty({ 5 | type: Boolean, 6 | example: true, 7 | }) 8 | result: boolean 9 | } 10 | -------------------------------------------------------------------------------- /src/controllers/dto/probe.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | 3 | export class ProbeDto { 4 | @ApiProperty() 5 | time: number 6 | } 7 | -------------------------------------------------------------------------------- /src/controllers/dto/sign.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { IsOptional, IsString } from 'class-validator' 3 | 4 | const signExample = 'MIAGCSqGSIb3DQEHAqCAMIACAQExDjAMBggqhQMHAQECAgUAMIAGCSqGSIb3DQEH\\nAaCAJIAEBXRlc3QKAAAAAAAAoIIDUDCCA0wwggL5oAMCAQICBQCl6ZkgMAoGCCqF\\nAwcBAQMCMIIBBTEgMB4GA1UEAwwX0JrQvtGB0LDRgNC10LIg0JjQstCw0L0xCzAJ\\nBgNVBAYTAlJVMRwwGgYDVQQIDBM3NyDQsy4g0JzQvtGB0LrQstCwMRUwEwYDVQQH\\nDAzQnNC+0YHQutCy0LAxOzA5BgNVBAkMMtCR0LXRgNGB0LXQvdGM0LXQstGB0LrQ\\nsNGPINC90LDQsS4sINC0LjYsINGB0YLRgC4zMS4wLAYDVQQKDCXQntCe0J4gItCS\\n0LXQsTMg0KLQtdGF0L3QvtC70L7Qs9C40LgiMRgwFgYFKoUDZAESDTExNzc3NDY4\\nNjM2NDUxGDAWBggqhQMDgQMBARIKNzcyNDQxNzQ0MDAeFw0yMTA3MTIxNTM5MTVa\\nFw0yMjA3MTIxNTM5MTVaMIIBBTEgMB4GA1UEAwwX0JrQvtGB0LDRgNC10LIg0JjQ\\nstCw0L0xCzAJBgNVBAYTAlJVMRwwGgYDVQQIDBM3NyDQsy4g0JzQvtGB0LrQstCw\\nMRUwEwYDVQQHDAzQnNC+0YHQutCy0LAxOzA5BgNVBAkMMtCR0LXRgNGB0LXQvdGM\\n0LXQstGB0LrQsNGPINC90LDQsS4sINC0LjYsINGB0YLRgC4zMS4wLAYDVQQKDCXQ\\nntCe0J4gItCS0LXQsTMg0KLQtdGF0L3QvtC70L7Qs9C40LgiMRgwFgYFKoUDZAES\\nDTExNzc3NDY4NjM2NDUxGDAWBggqhQMDgQMBARIKNzcyNDQxNzQ0MDBmMB8GCCqF\\nAwcBAQEBMBMGByqFAwICJAAGCCqFAwcBAQICA0MABECrgtyGqyZkhYgd/RZMdrhK\\n7Z2Dj9jx1le/NJBVu7OscHVTFG3y0pYLykXQOCxLsQqiN06U7GbFn9aFddI3o2Gw\\no0UwQzAOBgNVHQ8BAf8EBAMCA+gwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUF\\nBwMEMBIGA1UdEwEB/wQIMAYBAf8CAQUwCgYIKoUDBwEBAwIDQQDBkJnA6h/nEvDD\\nbvOV/TJGrL7t1W0cM2seDwQL1/veAKWOopHEAOs3/qSg+AJgoQ3g/hWvRouy2Hyj\\nqHbRV2YmMYIDUTCCA00CAQEwggEQMIIBBTEgMB4GA1UEAwwX0JrQvtGB0LDRgNC1\\n0LIg0JjQstCw0L0xCzAJBgNVBAYTAlJVMRwwGgYDVQQIDBM3NyDQsy4g0JzQvtGB\\n0LrQstCwMRUwEwYDVQQHDAzQnNC+0YHQutCy0LAxOzA5BgNVBAkMMtCR0LXRgNGB\\n0LXQvdGM0LXQstGB0LrQsNGPINC90LDQsS4sINC0LjYsINGB0YLRgC4zMS4wLAYD\\nVQQKDCXQntCe0J4gItCS0LXQsTMg0KLQtdGF0L3QvtC70L7Qs9C40LgiMRgwFgYF\\nKoUDZAESDTExNzc3NDY4NjM2NDUxGDAWBggqhQMDgQMBARIKNzcyNDQxNzQ0MAIF\\nAKXpmSAwDAYIKoUDBwEBAgIFAKCCAdQwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEH\\nATAcBgkqhkiG9w0BCQUxDxcNMjEwODEyMDk0NTAzWjAvBgkqhkiG9w0BCQQxIgQg\\nz0DjgIfibLR0lOoGmrY1OWnX48+eNLb8JERXcdK8KEQwggFnBgsqhkiG9w0BCRAC\\nLzGCAVYwggFSMIIBTjCCAUowCgYIKoUDBwEBAgIEIHWCJ8VR8QE/WM7w+qZY+6IY\\n+dAI+Y3cjQirSgNlmQ/aMIIBGDCCAQ2kggEJMIIBBTEgMB4GA1UEAwwX0JrQvtGB\\n0LDRgNC10LIg0JjQstCw0L0xCzAJBgNVBAYTAlJVMRwwGgYDVQQIDBM3NyDQsy4g\\n0JzQvtGB0LrQstCwMRUwEwYDVQQHDAzQnNC+0YHQutCy0LAxOzA5BgNVBAkMMtCR\\n0LXRgNGB0LXQvdGM0LXQstGB0LrQsNGPINC90LDQsS4sINC0LjYsINGB0YLRgC4z\\nMS4wLAYDVQQKDCXQntCe0J4gItCS0LXQsTMg0KLQtdGF0L3QvtC70L7Qs9C40Lgi\\nMRgwFgYFKoUDZAESDTExNzc3NDY4NjM2NDUxGDAWBggqhQMDgQMBARIKNzcyNDQx\\nNzQ0MAIFAKXpmSAwDAYIKoUDBwEBAQEFAARAIbVMPkWhdUlL6NHzfSMshAA842oA\\nbjtf0FY320oBVuUFhUk8LtTIxlgD2sI1uxUKqhawS16WDLgABtPt1PWfNwAAAAAA\\nAA==\\n' 5 | 6 | export class SignReqDto { 7 | @ApiProperty({ example: 'test' }) 8 | @IsString() 9 | text: string 10 | } 11 | 12 | export class VerifyTokensReq { 13 | @ApiProperty() 14 | @IsOptional() 15 | idToken: string 16 | 17 | @ApiProperty() 18 | @IsOptional() 19 | accessToken: string 20 | } 21 | 22 | export class VerifyReqDto { 23 | @ApiProperty({ example: signExample }) 24 | @IsString() 25 | sign: string 26 | } 27 | 28 | export class SignResDto { 29 | @ApiProperty({ example: signExample }) 30 | result: string 31 | } 32 | 33 | export class VerifyResDto { 34 | @ApiProperty({ type: Boolean, example: true }) 35 | result: boolean 36 | } 37 | 38 | export class UnsignResDto { 39 | @ApiProperty({ example: signExample }) 40 | result: string 41 | } 42 | -------------------------------------------------------------------------------- /src/controllers/dto/status.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | 3 | export class StatusDto { 4 | @ApiProperty() 5 | status: string 6 | 7 | @ApiProperty() 8 | version: string 9 | 10 | @ApiProperty() 11 | commit: string 12 | 13 | @ApiProperty() 14 | build: string 15 | 16 | @ApiProperty() 17 | tag: string 18 | } 19 | -------------------------------------------------------------------------------- /src/controllers/status.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common' 2 | import { BUILD_INFO } from '../config' 3 | import { ApiResponse } from '@nestjs/swagger' 4 | import { ProbeDto } from './dto/probe.dto' 5 | import { StatusDto } from './dto/status.dto' 6 | 7 | @Controller() 8 | export class StatusController { 9 | @Get('/status') 10 | @ApiResponse({ type: StatusDto }) 11 | getStatus() { 12 | return { 13 | status: 'OK', 14 | build: BUILD_INFO.BUILD_ID, 15 | commit: BUILD_INFO.GIT_COMMIT, 16 | tag: BUILD_INFO.DOCKER_TAG, 17 | version: BUILD_INFO.VERSION, 18 | } 19 | } 20 | 21 | @Get('livenessProbe') 22 | @ApiResponse({ 23 | status: 200, 24 | description: 'Liveness probe endpoint', 25 | type: ProbeDto, 26 | }) 27 | livenessProbe() { 28 | return { time: Date.now() } 29 | } 30 | 31 | @Get('readinessProbe') 32 | @ApiResponse({ 33 | status: 200, 34 | description: 'Readiness probe endpoint', 35 | type: ProbeDto, 36 | }) 37 | readinessProbe() { 38 | return { time: Date.now() } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express' 2 | import { NestFactory } from '@nestjs/core' 3 | import { AppModule } from './app.module' 4 | import { ValidationPipe } from '@nestjs/common' 5 | import { ExpressAdapter } from '@nestjs/platform-express' 6 | import { BUILD_INFO, isValidEnv, PORT, SWAGGER_BASE_PATH } from './config' 7 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' 8 | import { HttpExceptionFilter } from './middleware/http.exception.filter' 9 | import { ResultInterceptor } from './middleware/result.interceptor' 10 | import { log, logError } from './utils/logUtils' 11 | 12 | async function bootstrap() { 13 | log(`Build info: ${JSON.stringify(BUILD_INFO, null, 2)}`, 'Main') 14 | if (!isValidEnv()) { 15 | process.exit(1) 16 | return 17 | } 18 | const server = express() 19 | server.disable('x-powered-by') 20 | const app = await NestFactory.create( 21 | AppModule, 22 | new ExpressAdapter(server), 23 | { cors: { origin: ['*'], credentials: true } }, 24 | ) 25 | app.use(express.json({ limit: '50mb' })) 26 | app.useGlobalPipes( 27 | new ValidationPipe({ 28 | transform: true, 29 | }), 30 | ) 31 | app.useGlobalInterceptors(new ResultInterceptor()) 32 | app.useGlobalFilters(new HttpExceptionFilter()) 33 | 34 | const swaggerOptions = new DocumentBuilder() 35 | .setTitle('Voting CryptoPro service') 36 | .addServer(SWAGGER_BASE_PATH) 37 | .addBearerAuth() 38 | .build() 39 | 40 | const document = SwaggerModule.createDocument(app, swaggerOptions) 41 | await SwaggerModule.setup('/docs', app, document) 42 | 43 | await app.listen(PORT) 44 | } 45 | 46 | bootstrap() 47 | .catch((err) => { 48 | logError(err && err.message || err, err.stack, 'Main') 49 | process.exit(1) 50 | }) 51 | -------------------------------------------------------------------------------- /src/middleware/http.exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionFilter, ArgumentsHost, HttpException } from '@nestjs/common' 2 | import { logError } from '../utils/logUtils' 3 | import { Request, Response } from 'express' 4 | 5 | export class HttpExceptionFilter implements ExceptionFilter { 6 | catch(exception: HttpException | Error, host: ArgumentsHost) { 7 | const ctx = host.switchToHttp() 8 | const response = ctx.getResponse() 9 | const request = ctx.getRequest() 10 | 11 | // unhandled error 12 | if (typeof exception === 'string' || !('getStatus' in exception)) { 13 | const err = exception?.message ?? exception 14 | const body = JSON.stringify(request.body, null, 2) 15 | logError(`${request.url} ${err} req body: ${body}`, exception.stack, 'Unhandled rest exception') 16 | return response.status(500).json({ 17 | errorMessage: err, 18 | }) 19 | } 20 | 21 | const status = exception.getStatus() 22 | const exceptionResponse = exception.getResponse() as string | { message: string | string[] } 23 | 24 | // unknown error 25 | if (typeof exceptionResponse !== 'object') { 26 | return response.status(status).json({ errorMessage: exceptionResponse }) 27 | } 28 | 29 | // validation error 30 | if (Array.isArray(exceptionResponse.message)) { 31 | return response.status(status).json({ 32 | errorCode: 0, 33 | errorMessage: exceptionResponse.message.join(', '), 34 | }) 35 | } 36 | 37 | // known error 38 | return response.status(status).json(exceptionResponse) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/middleware/logger.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, HttpException, Injectable, NestInterceptor } from '@nestjs/common' 2 | import { Reflector } from '@nestjs/core' 3 | import { Observable } from 'rxjs' 4 | import { tap } from 'rxjs/operators' 5 | import { Request, Response } from 'express' 6 | import { log, logError } from '../utils/logUtils' 7 | 8 | @Injectable() 9 | export class LoggerInterceptor implements NestInterceptor { 10 | 11 | constructor( 12 | private readonly reflector: Reflector, 13 | ) { 14 | } 15 | 16 | intercept(context: ExecutionContext, next: CallHandler): Observable { 17 | const noLog = this.reflector.get('noLog', context.getHandler()) 18 | if (noLog) { 19 | return next.handle() 20 | } 21 | const removePasswordLogs = this.reflector.get('removePasswordLogs', context.getHandler()) 22 | const req: Request = context.switchToHttp().getRequest() 23 | const hasBody = ['post', 'put', 'patch', 'delete'].includes(req.method.toLowerCase()) 24 | const { originalUrl, body, method } = req 25 | 26 | return next 27 | .handle() 28 | .pipe( 29 | tap({ 30 | next: (): void => { 31 | const res: Response = context.switchToHttp().getResponse() 32 | const { statusCode } = res 33 | const message = `${method} ${originalUrl} [${statusCode}]` 34 | log(message, 'RequestLogger') 35 | }, 36 | error: (error: Error): void => { 37 | if (error instanceof HttpException) { 38 | const statusCode = error.getStatus() 39 | const errorMessage = error.message 40 | if (removePasswordLogs) { 41 | body.password = '*excluded*' 42 | } 43 | const bodyMessage = hasBody ? `Req body: ${JSON.stringify(body, null, 2)}` : '' 44 | 45 | const message = `${method} ${originalUrl} [${statusCode}] ${errorMessage} ${bodyMessage}` 46 | logError(message, '', 'RequestLogger') 47 | } 48 | }, 49 | }), 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/middleware/result.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' 2 | import { map } from 'rxjs/operators' 3 | 4 | @Injectable() 5 | export class ResultInterceptor implements NestInterceptor { 6 | intercept(_1: ExecutionContext, next: CallHandler) { 7 | return next.handle().pipe(map((result) => ({ result }))) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/types/errors.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common' 2 | 3 | export class InternalException extends HttpException { 4 | constructor(errorMessage: string | Record) { 5 | super( 6 | { 7 | errorMessage, 8 | errorCode: 1, 9 | }, 10 | 500, 11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/cryptoProUtils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import { logError } from './logUtils' 3 | import { CERTIFICATE_PIN } from '../config' 4 | import { InternalException } from '../types/errors' 5 | import * as tempy from 'tempy' 6 | import { readFile, unlink, writeFile } from 'fs/promises' 7 | import { dirname } from 'path' 8 | 9 | const execute = (command: string): Promise => { 10 | return new Promise((resolve, reject) => { 11 | exec(command, (err, stdout) => { 12 | if (err) { 13 | return reject(stdout || err.message) 14 | } else { 15 | resolve(stdout) 16 | } 17 | }) 18 | }) 19 | } 20 | 21 | let contrainerHash: string | null = null 22 | 23 | const getContainerHash = async () => { 24 | if (!contrainerHash) { 25 | const response = await execute('certmgr -list') 26 | const match = response.match(/SHA1 Hash\s*: (\w+)$/m) 27 | if (!match) { 28 | throw new InternalException('Cannot get container hash. It seems that service is not correctly configured') 29 | } 30 | contrainerHash = match[1] 31 | } 32 | return contrainerHash 33 | } 34 | 35 | export const cryptoProSign = async (str: string): Promise => { 36 | const containerHash = await getContainerHash() 37 | try { 38 | const tempFile = tempy.file({ extension: 'unsigned' }) 39 | const signedFile = tempFile + '.sgn' 40 | await writeFile(tempFile, str) 41 | const dirName = dirname(tempFile) 42 | // eslint-disable-next-line max-len 43 | const cmd = `cryptcp -signf -dir "${dirName}" -thumbprint "${containerHash}" -norev -nochain "${tempFile}" -cert -der -strict -hashAlg "1.2.643.7.1.1.2.2" -detached -pin "${CERTIFICATE_PIN}"` 44 | await execute(cmd) 45 | const result = await readFile(signedFile) 46 | await unlink(signedFile) 47 | await unlink(tempFile) 48 | return result.toString('base64') 49 | } catch (e) { 50 | logError(`sign error ${e}`, '', 'СryptoProSign') 51 | throw new InternalException('Failed to create sign. It seems that service is not correctly configured') 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/logUtils.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common' 2 | import { LOG_LEVEL } from '../config' 3 | 4 | const loggerService = new Logger() 5 | 6 | export const logError = (message: unknown, trace?: string, context?: string) => { 7 | if (!LOG_LEVEL.has('error')) { 8 | return 9 | } 10 | loggerService.error(message, trace, context) 11 | } 12 | 13 | export const logWarn = (message: unknown, context?: string) => { 14 | if (!LOG_LEVEL.has('warn')) { 15 | return 16 | } 17 | loggerService.warn(message, context) 18 | } 19 | 20 | export const log = (message: unknown, context?: string) => { 21 | if (!LOG_LEVEL.has('log')) { 22 | return 23 | } 24 | loggerService.log(message, context) 25 | } 26 | 27 | export const logDebug = (message: unknown, context?: string) => { 28 | if (!LOG_LEVEL.has('debug')) { 29 | return 30 | } 31 | loggerService.debug(message, context) 32 | } 33 | -------------------------------------------------------------------------------- /test/cryptopro.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing' 2 | import { CryptoProController } from '../src/controllers/cryptopro.controller' 3 | 4 | describe('CryptoProController', () => { 5 | let controller: CryptoProController 6 | 7 | beforeAll(async () => { 8 | const app: TestingModule = await Test.createTestingModule({ 9 | controllers: [CryptoProController], 10 | providers: [], 11 | }).compile() 12 | controller = app.get(CryptoProController) 13 | }) 14 | 15 | describe('sign', () => { 16 | it('should successfully sign text', async () => { 17 | const sign = await controller.sign({ text: 'test' }) 18 | expect({ sign }).toEqual({ 19 | sign: expect.any(String), 20 | }) 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/status.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing' 2 | import { StatusController } from '../src/controllers/status.controller' 3 | 4 | describe('AppController (health methods)', () => { 5 | let appController: StatusController 6 | 7 | beforeAll(async () => { 8 | const app: TestingModule = await Test.createTestingModule({ 9 | controllers: [StatusController], 10 | providers: [], 11 | }).compile() 12 | appController = app.get(StatusController) 13 | }) 14 | 15 | it('should return status', () => { 16 | const { status } = appController.getStatus() 17 | expect(status).toBe('OK') 18 | }) 19 | 20 | it('should return livenessProbe', () => { 21 | const { time } = appController.livenessProbe() 22 | expect(time).toBeDefined() 23 | }) 24 | 25 | it('should return readinessProbe', () => { 26 | const { time } = appController.readinessProbe() 27 | expect(time).toBeDefined() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 7 | }, 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "strictPropertyInitialization": false, 5 | "target": "es2019", 6 | "lib": ["es2019"], 7 | "jsx": "react", 8 | "moduleResolution": "node", 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "removeComments": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true, 14 | "resolveJsonModule": true, 15 | "outDir": "distTs", 16 | "baseUrl": ".", 17 | "allowSyntheticDefaultImports": true, 18 | "skipLibCheck": true 19 | }, 20 | "typeAcquisition": { 21 | "include": ["jest"] 22 | }, 23 | "exclude": ["node_modules", "dist"] 24 | } 25 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | {} --------------------------------------------------------------------------------